diff --git a/codebundles/aws-c7n-security-hub/.runwhen/generation-rules/aws-c7n-security-hub.yaml b/codebundles/aws-c7n-security-hub/.runwhen/generation-rules/aws-c7n-security-hub.yaml new file mode 100644 index 0000000..ebe2ed7 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.runwhen/generation-rules/aws-c7n-security-hub.yaml @@ -0,0 +1,22 @@ +apiVersion: runwhen.com/v1 +kind: GenerationRules +spec: + platform: aws + generationRules: + - resourceTypes: + - aws_securityhub_hubs + matchRules: + - type: pattern + pattern: ".+" + properties: [name] + mode: substring + slxs: + - baseName: aws-c7n-security-hub + qualifiers: ["account_id"] + baseTemplateName: aws-c7n-security-hub + levelOfDetail: basic + outputItems: + - type: slx + - type: sli + - type: runbook + templateName: aws-c7n-security-hub-taskset.yaml \ No newline at end of file diff --git a/codebundles/aws-c7n-security-hub/.runwhen/templates/aws-c7n-security-hub-sli.yaml b/codebundles/aws-c7n-security-hub/.runwhen/templates/aws-c7n-security-hub-sli.yaml new file mode 100644 index 0000000..0b28a09 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.runwhen/templates/aws-c7n-security-hub-sli.yaml @@ -0,0 +1,51 @@ +apiVersion: runwhen.com/v1 +kind: ServiceLevelIndicator +metadata: + name: {{slx_name}} + labels: + {% include "common-labels.yaml" %} + annotations: + {% include "common-annotations.yaml" %} +spec: + displayUnitsLong: OK + displayUnitsShort: ok + locations: + - {{default_location}} + description: Check for AWS Security Hub findings in AWS account {{match_resource.resource.account_id}} + codeBundle: + {% if repo_url %} + repoUrl: {{repo_url}} + {% else %} + repoUrl: https://github.com/runwhen-contrib/rw-c7n-codecollection.git + {% endif %} + {% if ref %} + ref: {{ref}} + {% else %} + ref: main + {% endif %} + pathToRobot: codebundles/aws-c7n-security-hub/sli.robot + intervalStrategy: intermezzo + intervalSeconds: 600 + configProvided: + - name: AWS_REGION + value: "{{match_resource.resource.region}}" + - name: AWS_ACCOUNT_ID + value: "{{match_resource.resource.account_id}}" + secretsProvided: + - name: AWS_ACCESS_KEY_ID + workspaceKey: {{custom.aws_access_key_id}} + - name: AWS_SECRET_ACCESS_KEY + workspaceKey: {{custom.aws_secret_access_key}} + alerts: + warning: + operator: '>' + threshold: '1' + for: '20m' + ticket: + operator: '>' + threshold: '1' + for: '40m' + page: + operator: '==' + threshold: '0' + for: '' diff --git a/codebundles/aws-c7n-security-hub/.runwhen/templates/aws-c7n-security-hub-slx.yaml b/codebundles/aws-c7n-security-hub/.runwhen/templates/aws-c7n-security-hub-slx.yaml new file mode 100644 index 0000000..626006f --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.runwhen/templates/aws-c7n-security-hub-slx.yaml @@ -0,0 +1,21 @@ +apiVersion: runwhen.com/v1 +kind: ServiceLevelX +metadata: + name: {{slx_name}} + labels: + {% include "common-labels.yaml" %} + annotations: + {% include "common-annotations.yaml" %} +spec: + imageURL: https://storage.googleapis.com/runwhen-nonprod-shared-images/icons/aws/Resource-Icons_06072024/Res_Security-Identity-Compliance/Res_AWS-Security-Hub_Finding_48.svg + alias: AWS Security Hub findings in AWS Account {{match_resource.resource.account_id}} + asMeasuredBy: The number of AWS Security Hub findings in AWS account {{match_resource.resource.account_id}} + configProvided: + - name: SLX_PLACEHOLDER + value: SLX_PLACEHOLDER + owners: + - {{workspace.owner_email}} + statement: List AWS Security Hub findings in the AWS account {{match_resource.resource.account_id}} + additionalContext: + region: "{{match_resource.resource.region}}" + account_id: "{{match_resource.resource.account_id}}" \ No newline at end of file diff --git a/codebundles/aws-c7n-security-hub/.runwhen/templates/aws-c7n-security-hub-taskset.yaml b/codebundles/aws-c7n-security-hub/.runwhen/templates/aws-c7n-security-hub-taskset.yaml new file mode 100644 index 0000000..78ec5e4 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.runwhen/templates/aws-c7n-security-hub-taskset.yaml @@ -0,0 +1,33 @@ +apiVersion: runwhen.com/v1 +kind: Runbook +metadata: + name: {{slx_name}} + labels: + {% include "common-labels.yaml" %} + annotations: + {% include "common-annotations.yaml" %} +spec: + location: {{default_location}} + description: List Security Hub findings in the AWS account {{match_resource.resource.account_id}} + codeBundle: + {% if repo_url %} + repoUrl: {{repo_url}} + {% else %} + repoUrl: https://github.com/runwhen-contrib/rw-c7n-codecollection.git + {% endif %} + {% if ref %} + ref: {{ref}} + {% else %} + ref: main + {% endif %} + pathToRobot: codebundles/aws-c7n-security-hub/runbook.robot + configProvided: + - name: AWS_REGION + value: "{{match_resource.resource.region}}" + - name: AWS_ACCOUNT_ID + value: "{{match_resource.resource.account_id}}" + secretsProvided: + - name: AWS_ACCESS_KEY_ID + workspaceKey: {{custom.aws_access_key_id}} + - name: AWS_SECRET_ACCESS_KEY + workspaceKey: {{custom.aws_secret_access_key}} diff --git a/codebundles/aws-c7n-security-hub/.test/README.md b/codebundles/aws-c7n-security-hub/.test/README.md new file mode 100644 index 0000000..b4e8daa --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.test/README.md @@ -0,0 +1,102 @@ +### How to test this codebundle? + +#### IAM User Configuration + +We create two distinct AWS IAM users with carefully scoped access: + +**CloudCustodian IAM User** + +Purpose: Service Level Indicator (SLI) monitoring and runbook automation and configured with least privilege access principles + +With the following policy: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "tag:GetResources", + "securityhub:GetFindings", + "s3:List*", + "s3:Get*", + "ec2:Describe*", + "iam:List*", + "iam:Get*", + "rds:Describe*", + "cloudwatch:Get*", + "cloudformation:Describe*", + "dynamodb:Scan", + "dynamodb:Describe*", + "lambda:List*", + "lambda:Get*", + "sns:List*" + ], + "Resource": "*" + } + ] +} +``` +**Note** Please ensure to update the policy whenever new resources are added to the `AWS_RESOURCE_PROVIDERS` list in the `sli.robot` and `runbook.robot` + +**Infrastructure Deployment User** + +Purpose: Cloud infrastructure provisioning and management using Terraform + +#### Credential Setup + +Navigate to the `.test/terraform` directory and configure two secret files for authentication: + +`cb.secret` - CloudCustodian and RunWhen Credentials + +Create this file with the following environment variables: + + ```sh + export RW_PAT="" + export RW_WORKSPACE="" + export RW_API_URL="papi.beta.runwhen.com" + + export AWS_DEFAULT_REGION="us-west-2" + export AWS_ACCESS_KEY_ID="" + export AWS_SECRET_ACCESS_KEY="" + export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text) + ``` + + +`tf.secret` - Terraform Deployment Credentials + +Create this file with the following environment variables: + + ```sh + export AWS_DEFAULT_REGION="" + export AWS_ACCESS_KEY_ID="" + export AWS_SECRET_ACCESS_KEY="" + export AWS_SESSION_TOKEN="" # Optional: Include if using temporary credentials + ``` + +#### Testing Workflow + +1. Build test infra: + ```sh + task build-infra + ``` + +2. Generate RunWhen Configurations + ```sh + tasks + ``` + +3. Upload generated SLx to RunWhen Platform + + ```sh + task upload-slxs + ``` + +4. At last, after testing, clean up the test infrastructure. + + ```sh + task clean + ``` + diff --git a/codebundles/aws-c7n-security-hub/.test/Taskfile.yaml b/codebundles/aws-c7n-security-hub/.test/Taskfile.yaml new file mode 100644 index 0000000..4319299 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.test/Taskfile.yaml @@ -0,0 +1,352 @@ +version: "3" + +tasks: + default: + desc: "Generate workspaceInfo and rebuild/test" + cmds: + - task: check-unpushed-commits + - task: generate-rwl-config + - task: run-rwl-discovery + + clean: + desc: "Run cleanup tasks" + cmds: + - task: check-and-cleanup-terraform + # - task: clean-rwl-discovery + + build-infra: + desc: "Build test infrastructure" + cmds: + - task: build-terraform-infra + + check-unpushed-commits: + desc: Check if outstanding commits or file updates need to be pushed before testing. + vars: + # Specify the base directory relative to your Taskfile location + BASE_DIR: "../" + cmds: + - | + echo "Checking for uncommitted changes in $BASE_DIR and $BASE_DIR.runwhen, excluding '.test'..." + UNCOMMITTED_FILES=$(git diff --name-only HEAD | grep -E "^${BASE_DIR}(\.runwhen|[^/]+)" | grep -v "/\.test/" || true) + if [ -n "$UNCOMMITTED_FILES" ]; then + echo "✗" + echo "Uncommitted changes found:" + echo "$UNCOMMITTED_FILES" + echo "Remember to commit & push changes before executing the `run-rwl-discovery` task." + echo "------------" + exit 1 + else + echo "√" + echo "No uncommitted changes in specified directories." + echo "------------" + fi + - | + echo "Checking for unpushed commits in $BASE_DIR and $BASE_DIR.runwhen, excluding '.test'..." + git fetch origin + UNPUSHED_FILES=$(git diff --name-only origin/$(git rev-parse --abbrev-ref HEAD) HEAD | grep -E "^${BASE_DIR}(\.runwhen|[^/]+)" | grep -v "/\.test/" || true) + if [ -n "$UNPUSHED_FILES" ]; then + echo "✗" + echo "Unpushed commits found:" + echo "$UNPUSHED_FILES" + echo "Remember to push changes before executing the `run-rwl-discovery` task." + echo "------------" + exit 1 + else + echo "√" + echo "No unpushed commits in specified directories." + echo "------------" + fi + silent: true + + generate-rwl-config: + desc: "Generate RunWhen Local configuration (workspaceInfo.yaml)" + env: + AWS_ACCESS_KEY_ID: "{{.AWS_ACCESS_KEY_ID}}" + AWS_SECRET_ACCESS_KEY: "{{.AWS_SECRET_ACCESS_KEY}}" + AWS_DEFAULT_REGION: "{{.AWS_DEFAULT_REGION}}" + RW_WORKSPACE: '{{.RW_WORKSPACE | default "my-workspace"}}' + cmds: + - | + source terraform/cb.secret + repo_url=$(git config --get remote.origin.url) + branch_name=$(git rev-parse --abbrev-ref HEAD) + codebundle=$(basename "$(dirname "$PWD")") + + # Fetch individual cluster details from Terraform state + # pushd terraform > /dev/null + # cluster1_name=$(terraform show -json terraform.tfstate | jq -r ' + # .values.outputs.cluster_1_name.value') + + # popd > /dev/null + + # # Check if any of the required cluster variables are empty + # if [ -z "$cluster1_name" ] || [ -z "$cluster1_server" ] || [ -z "$cluster1_resource_group" ]; then + # echo "Error: Missing cluster details. Ensure Terraform plan has been applied." + # exit 1 + # fi + + # Generate workspaceInfo.yaml with fetched cluster details + cat < workspaceInfo.yaml + workspaceName: "$RW_WORKSPACE" + workspaceOwnerEmail: authors@runwhen.com + defaultLocation: location-01-us-west1 + defaultLOD: detailed + cloudConfig: + aws: + awsAccessKeyId: "$AWS_ACCESS_KEY_ID" + awsSecretAccessKey: "$AWS_SECRET_ACCESS_KEY" + codeCollections: + - repoURL: "$repo_url" + branch: "$branch_name" + codeBundles: ["$codebundle"] + custom: + aws_access_key_id: AWS_ACCESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + EOF + silent: true + + run-rwl-discovery: + desc: "Run RunWhen Local Discovery on test infrastructure" + cmds: + - | + CONTAINER_NAME="RunWhenLocal" + if docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then + echo "Stopping and removing existing container $CONTAINER_NAME..." + docker stop $CONTAINER_NAME && docker rm $CONTAINER_NAME + elif docker ps -a -q --filter "name=$CONTAINER_NAME" | grep -q .; then + echo "Removing existing stopped container $CONTAINER_NAME..." + docker rm $CONTAINER_NAME + else + echo "No existing container named $CONTAINER_NAME found." + fi + + echo "Cleaning up output directory..." + sudo rm -rf output || { echo "Failed to remove output directory"; exit 1; } + mkdir output && chmod 777 output || { echo "Failed to set permissions"; exit 1; } + + echo "Starting new container $CONTAINER_NAME..." + + docker run --name $CONTAINER_NAME -e DEBUG_LOGGING=true -p 8081:8081 -v "$(pwd)":/shared -d ghcr.io/runwhen-contrib/runwhen-local:latest || { + echo "Failed to start container"; exit 1; + } + + echo "Running workspace builder script in container..." + docker exec -w /workspace-builder $CONTAINER_NAME ./run.sh $1 --verbose || { + echo "Error executing script in container"; exit 1; + } + + echo "Review generated config files under output/workspaces/" + silent: true + + check-terraform-infra: + desc: "Check if Terraform has any deployed infrastructure in the terraform subdirectory" + cmds: + - | + # Source Envs for Auth + source terraform/tf.secret + + # Navigate to the Terraform directory + if [ ! -d "terraform" ]; then + echo "Terraform directory not found." + exit 1 + fi + cd terraform + + # Check if Terraform state file exists + if [ ! -f "terraform.tfstate" ]; then + echo "No Terraform state file found in the terraform directory. No infrastructure is deployed." + exit 0 + fi + + # List resources in Terraform state + resources=$(terraform state list) + + # Check if any resources are listed in the state file + if [ -n "$resources" ]; then + echo "Deployed infrastructure detected." + echo "$resources" + exit 0 + else + echo "No deployed infrastructure found in Terraform state." + exit 0 + fi + silent: true + + build-terraform-infra: + desc: "Run terraform apply" + cmds: + - | + # Source Envs for Auth + source terraform/tf.secret + + + # Navigate to the Terraform directory + if [ -d "terraform" ]; then + cd terraform + else + echo "Terraform directory not found. Terraform apply aborted." + exit 1 + fi + task format-and-init-terraform + echo "Starting Terraform Build of Terraform infrastructure..." + terraform apply -auto-approve || { + echo "Failed to clean up Terraform infrastructure." + exit 1 + } + echo "Terraform infrastructure build completed." + silent: true + + check-rwp-config: + desc: Check if env vars are set for RunWhen Platform + cmds: + - | + source terraform/cb.secret + missing_vars=() + + if [ -z "$RW_WORKSPACE" ]; then + missing_vars+=("RW_WORKSPACE") + fi + + if [ -z "$RW_API_URL" ]; then + missing_vars+=("RW_API_URL") + fi + + if [ -z "$RW_PAT" ]; then + missing_vars+=("RW_PAT") + fi + + if [ ${#missing_vars[@]} -ne 0 ]; then + echo "The following required environment variables are missing: ${missing_vars[*]}" + exit 1 + fi + silent: true + + upload-slxs: + desc: "Upload SLX files to the appropriate URL" + env: + RW_WORKSPACE: "{{.RW_WORKSPACE}}" + RW_API_URL: "{{.RW_API}}" + RW_PAT: "{{.RW_PAT}}" + cmds: + - task: check-rwp-config + - | + source terraform/cb.secret + BASE_DIR="output/workspaces/${RW_WORKSPACE}/slxs" + if [ ! -d "$BASE_DIR" ]; then + echo "Directory $BASE_DIR does not exist. Upload aborted." + exit 1 + fi + + for dir in "$BASE_DIR"/*; do + if [ -d "$dir" ]; then + SLX_NAME=$(basename "$dir") + PAYLOAD=$(jq -n --arg commitMsg "Creating new SLX $SLX_NAME" '{ commitMsg: $commitMsg, files: {} }') + for file in slx.yaml runbook.yaml sli.yaml; do + if [ -f "$dir/$file" ]; then + CONTENT=$(cat "$dir/$file") + PAYLOAD=$(echo "$PAYLOAD" | jq --arg fileContent "$CONTENT" --arg fileName "$file" '.files[$fileName] = $fileContent') + fi + done + + URL="https://${RW_API_URL}/api/v3/workspaces/${RW_WORKSPACE}/branches/main/slxs/${SLX_NAME}" + echo "Uploading SLX: $SLX_NAME to $URL" + response_code=$(curl -X POST "$URL" \ + -H "Authorization: Bearer $RW_PAT" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" \ + -w "%{http_code}" -o /dev/null -s) + + if [[ "$response_code" == "200" || "$response_code" == "201" ]]; then + echo "Successfully uploaded SLX: $SLX_NAME to $URL" + elif [[ "$response_code" == "405" ]]; then + echo "Failed to upload SLX: $SLX_NAME to $URL. Method not allowed (405)." + else + echo "Failed to upload SLX: $SLX_NAME to $URL. Unexpected response code: $response_code" + fi + fi + done + silent: true + + delete-slxs: + desc: "Delete SLX objects from the appropriate URL" + env: + RW_WORKSPACE: '{{.RW_WORKSPACE | default "my-workspace"}}' + RW_API_URL: "{{.RW_API}}" + RW_PAT: "{{.RW_PAT}}" + cmds: + - task: check-rwp-config + - | + source terraform/cb.secret + BASE_DIR="output/workspaces/${RW_WORKSPACE}/slxs" + if [ ! -d "$BASE_DIR" ]; then + echo "Directory $BASE_DIR does not exist. Deletion aborted." + exit 1 + fi + + for dir in "$BASE_DIR"/*; do + if [ -d "$dir" ]; then + SLX_NAME=$(basename "$dir") + URL="https://${RW_API_URL}/api/v3/workspaces/${RW_WORKSPACE}/branches/main/slxs/${SLX_NAME}" + echo "Deleting SLX: $SLX_NAME from $URL" + response_code=$(curl -X DELETE "$URL" \ + -H "Authorization: Bearer $RW_PAT" \ + -H "Content-Type: application/json" \ + -w "%{http_code}" -o /dev/null -s) + + if [[ "$response_code" == "200" || "$response_code" == "204" ]]; then + echo "Successfully deleted SLX: $SLX_NAME from $URL" + elif [[ "$response_code" == "405" ]]; then + echo "Failed to delete SLX: $SLX_NAME from $URL. Method not allowed (405)." + else + echo "Failed to delete SLX: $SLX_NAME from $URL. Unexpected response code: $response_code" + fi + fi + done + silent: true + + cleanup-terraform-infra: + desc: "Cleanup deployed Terraform infrastructure" + cmds: + - | + # Source Envs for Auth + source terraform/tf.secret + + # Navigate to the Terraform directory + if [ -d "terraform" ]; then + cd terraform + else + echo "Terraform directory not found. Cleanup aborted." + exit 1 + fi + + echo "Starting cleanup of Terraform infrastructure..." + terraform destroy -auto-approve || { + echo "Failed to clean up Terraform infrastructure." + exit 1 + } + echo "Terraform infrastructure cleanup completed." + silent: true + + check-and-cleanup-terraform: + desc: "Check and clean up deployed Terraform infrastructure if it exists" + cmds: + - | + # Capture the output of check-terraform-infra + infra_output=$(task check-terraform-infra | tee /dev/tty) + + # Check if output contains indication of deployed infrastructure + if echo "$infra_output" | grep -q "Deployed infrastructure detected"; then + echo "Infrastructure detected; proceeding with cleanup." + task cleanup-terraform-infra + else + echo "No deployed infrastructure found; no cleanup required." + fi + silent: true + + clean-rwl-discovery: + desc: "Check and clean up RunWhen Local discovery output" + cmds: + - | + sudo rm -rf output + rm workspaceInfo.yaml + silent: true diff --git a/codebundles/aws-c7n-security-hub/.test/terraform/Taskfile.yaml b/codebundles/aws-c7n-security-hub/.test/terraform/Taskfile.yaml new file mode 100644 index 0000000..08e0e83 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.test/terraform/Taskfile.yaml @@ -0,0 +1,69 @@ +version: '3' + +env: + TERM: screen-256color + +tasks: + default: + cmds: + - task: test + + test: + desc: Run tests. + cmds: + - task: test-terraform + + clean: + desc: Clean the environment. + cmds: + - task: clean-go + - task: clean-terraform + + clean-terraform: + desc: Clean the terraform environment (remove terraform directories and files) + cmds: + - find . -type d -name .terraform -exec rm -rf {} + + - find . -type f -name .terraform.lock.hcl -delete + + format-and-init-terraform: + desc: Run Terraform fmt and init + cmds: + - | + terraform fmt + terraform init + test-terraform: + desc: Run tests for all terraform directories. + silent: true + env: + DIRECTORIES: + sh: find . -path '*/.terraform/*' -prune -o -name '*.tf' -type f -exec dirname {} \; | sort -u + cmds: + - | + BOLD=$(tput bold) + NORM=$(tput sgr0) + + CWD=$PWD + + for d in $DIRECTORIES; do + cd $d + echo "${BOLD}$PWD:${NORM}" + if ! terraform fmt -check=true -list=false -recursive=false; then + echo " ✗ terraform fmt" && exit 1 + else + echo " √ terraform fmt" + fi + + if ! terraform init -backend=false -input=false -get=true -no-color > /dev/null; then + echo " ✗ terraform init" && exit 1 + else + echo " √ terraform init" + fi + + if ! terraform validate > /dev/null; then + echo " ✗ terraform validate" && exit 1 + else + echo " √ terraform validate" + fi + + cd $CWD + done \ No newline at end of file diff --git a/codebundles/aws-c7n-security-hub/.test/terraform/main.tf b/codebundles/aws-c7n-security-hub/.test/terraform/main.tf new file mode 100644 index 0000000..f9902af --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.test/terraform/main.tf @@ -0,0 +1,14 @@ +# Create the EBS volume if it doesn't exist +resource "aws_ebs_volume" "ebs_volume" { + availability_zone = var.availability_zone + size = var.ebs_volume_size + encrypted = false + tags = { + Name = var.ebs_volume_name + } +} + +# Output +output "volume_id" { + value = aws_ebs_volume.ebs_volume.id +} \ No newline at end of file diff --git a/codebundles/aws-c7n-security-hub/.test/terraform/provider.tf b/codebundles/aws-c7n-security-hub/.test/terraform/provider.tf new file mode 100644 index 0000000..aa39e39 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.test/terraform/provider.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = "us-west-2" # Replace with your desired region +} \ No newline at end of file diff --git a/codebundles/aws-c7n-security-hub/.test/terraform/vars.tf b/codebundles/aws-c7n-security-hub/.test/terraform/vars.tf new file mode 100644 index 0000000..819a450 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/.test/terraform/vars.tf @@ -0,0 +1,25 @@ +# Variables +variable "ebs_volume_name" { + default = "ebs-sh" +} + +variable "ebs_volume_size" { + default = 1 +} + +variable "region" { + default = "us-west-2" +} + +variable "availability_zone" { + default = "us-west-2b" +} + + +variable "snapshot_volume_name" { + default = "ebs-snapshot-sh" +} + +variable "snapshot_volume_size" { + default = 1 +} \ No newline at end of file diff --git a/codebundles/aws-c7n-security-hub/README.md b/codebundles/aws-c7n-security-hub/README.md new file mode 100644 index 0000000..fa490a5 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/README.md @@ -0,0 +1,25 @@ +# AWS Cloud Custodian Security Hub + +This CodeBundle evaluates the Security Hub Findings in a given AWS Account and Region + +## SLI +The SLI produces a score of 0 (bad), 1(good), or a value in between. This score is generated by capturing the following: +- Security Hub Findings + + +## TaskSet +Similar to the SLI, but produces a report on the specific resources and raises issues for each Security Hub Findings that requires attention. + + +## Required Configuration + +``` +export AWS_ACCESS_KEY_ID=[] +export AWS_SECRET_ACCESS_KEY=[] +export AWS_DEFAULT_REGION=[] +export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text) +``` + + +## Testing +See the .test directory for infrastructure test code. \ No newline at end of file diff --git a/codebundles/aws-c7n-security-hub/runbook.robot b/codebundles/aws-c7n-security-hub/runbook.robot new file mode 100644 index 0000000..d4aee52 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/runbook.robot @@ -0,0 +1,120 @@ +*** Settings *** +Metadata Author saurabh3460 +Metadata Supports AWS Tag CloudCustodian +Metadata Display Name AWS Security Hub +Documentation List aws security hub findings +Force Tags Tag AWS security-hub + +Library RW.Core +Library RW.CLI +Library CloudCustodian.Core + +Suite Setup Suite Initialization + +*** Tasks *** +List aws security hub findings in AWS account `${AWS_ACCOUNT_ID}` + [Documentation] Check for security hub findings + [Tags] aws security-hub + CloudCustodian.Core.Generate Policy + ... ${CURDIR}/security-hub.j2 + ... resource_providers=${AWS_RESOURCE_PROVIDERS} + ${total_count}= Set Variable 0 + FOR ${region} IN @{AWS_ENABLED_REGIONS} + ${c7n_output}= RW.CLI.Run Cli + ... cmd=custodian run -r ${region} --output-dir ${OUTPUT_DIR}/aws-c7n-security-findings/${region} ${CURDIR}/security-hub.yaml --cache-period 0 + ... secret__aws_access_key_id=${AWS_ACCESS_KEY_ID} + ... secret__aws_secret_access_key=${AWS_SECRET_ACCESS_KEY} + ... timeout_seconds=120 + ${dirs}= RW.CLI.Run Cli + ... cmd=find ${OUTPUT_DIR}/aws-c7n-security-findings/${region} -mindepth 1 -maxdepth 1 -type d | jq -R -s 'split("\n") | map(select(length > 0))'; + TRY + ${dir_list}= Evaluate json.loads(r'''${dirs.stdout}''') json + EXCEPT + Log Failed to load JSON payload, defaulting to empty list. WARN + END + + IF len(@{dir_list}) > 0 + FOR ${dir} IN @{dir_list} + ${product}= Evaluate "${dir}".rstrip('/').split('/')[-1].removesuffix('-findings') + + ${report_data}= RW.CLI.Run Cli + ... cmd=cat ${dir}/resources.json + + TRY + ${resource_list}= Evaluate json.loads(r'''${report_data.stdout}''') json + EXCEPT + Log Failed to load JSON payload, defaulting to empty list. WARN + ${resource_list}= Create List + END + + ${report}= RW.CLI.Run Cli + ... cmd=jq '.[] | { Findings: ( .["c7n:finding-filter"][] | { Title: .Title, ProductName: .ProductName, Description: .Description, Resources: ( .Resources[] | { Type: .Type, Id: .Id, Region: .Region } ) })}' ${dir}/resources.json + Log ${report.stdout} + IF $report.stdout != "" + RW.Core.Add Pre To Report ${report.stdout} + ELSE + RW.Core.Add Pre To Report "No Security Hub Findings in AWS region ${region} for ${product}" + END + + IF len(@{resource_list}) > 0 + # Generate and format report + FOR ${item} IN @{resource_list} + FOR ${finding} IN @{item['c7n:finding-filter']} + ${pretty_finding}= Evaluate pprint.pformat(${finding}) modules=pprint + ${severity_label}= Set Variable ${finding['Severity']['Label']} + ${severity}= Evaluate 1 if '${severity_label}' == 'CRITICAL' else 2 if '${severity_label}' == 'HIGH' else 3 if '${severity_label}' == 'MEDIUM' else 4 + FOR ${resource} IN @{finding['Resources']} + RW.Core.Add Issue + ... severity=${severity} + ... expected=${resource['Type']} ${resource['Id']} in AWS Region `${region}` in AWS Account `${AWS_ACCOUNT_ID}` should follow `${finding['Title']}` + ... actual=AWS Security Hub detected an issue with the rule `${finding['Title']}` for `${resource['Type']}` `${resource['Id']}` in AWS Region `${region}` and AWS Account `${AWS_ACCOUNT_ID}` + ... title=Security issue detected: Rule `${finding['Title']}` violated by `${resource['Type']}` `${resource['Id']}` in AWS Region `${region}` and AWS Account `${AWS_ACCOUNT_ID}` + ... reproduce_hint=${c7n_output.cmd} + ... details=${pretty_finding} + ... next_steps=Review security hub findings in report related to rule `${finding['Title']}` on resource `${resource['Type']}` `${resource['Id']}` in AWS Region `${region}` and AWS Account `${AWS_ACCOUNT_ID}` + END + END + END + END + END + ELSE + RW.Core.Add Pre To Report "No directories found to process" + END + END + +** Keywords *** +Suite Initialization + ${AWS_REGION}= RW.Core.Import User Variable AWS_REGION + ... type=string + ... description=AWS Region + ... pattern=\w* + ${AWS_ACCOUNT_ID}= RW.Core.Import User Variable AWS_ACCOUNT_ID + ... type=string + ... description=AWS Account ID + ... pattern=\w* + ${AWS_ACCESS_KEY_ID}= RW.Core.Import Secret AWS_ACCESS_KEY_ID + ... type=string + ... description=AWS Access Key ID + ... pattern=\w* + ${AWS_SECRET_ACCESS_KEY}= RW.Core.Import Secret AWS_SECRET_ACCESS_KEY + ... type=string + ... description=AWS Access Key Secret + ... pattern=\w* + ${AWS_RESOURCE_PROVIDERS}= RW.Core.Import User Variable AWS_RESOURCE_PROVIDERS + ... type=string + ... description=Comma separated list of AWS Resource Providers. + ... pattern=^[a-zA-Z0-9,]+$ + ... example="ec2,s3,rds,vpc" + ... default="ec2,s3,rds,vpc,ebs,iam-group,iam-policy,iam-role,iam-user" + ${clean_workding_dir}= RW.CLI.Run Cli cmd=rm -rf ${OUTPUT_DIR}/aws-c7n-security-findings + ${AWS_ENABLED_REGIONS}= RW.CLI.Run Cli + ... cmd=aws ec2 describe-regions --region ${AWS_REGION} --query 'Regions[*].RegionName' --output json + ... secret__aws_access_key_id=${AWS_ACCESS_KEY_ID} + ... secret__aws_secret_access_key=${AWS_SECRET_ACCESS_KEY} + ${AWS_ENABLED_REGIONS}= Evaluate json.loads(r'''${AWS_ENABLED_REGIONS.stdout}''') json + Set Suite Variable ${AWS_ENABLED_REGIONS} ${AWS_ENABLED_REGIONS} + Set Suite Variable ${AWS_REGION} ${AWS_REGION} + Set Suite Variable ${AWS_ACCOUNT_ID} ${AWS_ACCOUNT_ID} + Set Suite Variable ${AWS_ACCESS_KEY_ID} ${AWS_ACCESS_KEY_ID} + Set Suite Variable ${AWS_SECRET_ACCESS_KEY} ${AWS_SECRET_ACCESS_KEY} + Set Suite Variable ${AWS_RESOURCE_PROVIDERS} ${AWS_RESOURCE_PROVIDERS} \ No newline at end of file diff --git a/codebundles/aws-c7n-security-hub/security-hub.j2 b/codebundles/aws-c7n-security-hub/security-hub.j2 new file mode 100644 index 0000000..bfe3a46 --- /dev/null +++ b/codebundles/aws-c7n-security-hub/security-hub.j2 @@ -0,0 +1,7 @@ +policies: +{%- for provider in resource_providers.split(',') %} + - name: {{ provider.strip('"').strip("'").strip() }}-findings + resource: {{ provider.strip('"').strip("'").strip() }} + filters: + - finding +{%- endfor %} \ No newline at end of file diff --git a/codebundles/aws-c7n-security-hub/sli.robot b/codebundles/aws-c7n-security-hub/sli.robot new file mode 100644 index 0000000..be00a1b --- /dev/null +++ b/codebundles/aws-c7n-security-hub/sli.robot @@ -0,0 +1,84 @@ +*** Settings *** +Metadata Author saurabh3460 +Metadata Supports AWS Tag CloudCustodian +Metadata Display Name AWS Security Hub +Documentation Check for aws security hub findings +Force Tags Tag AWS security-hub + +Library RW.Core +Library RW.CLI +Library CloudCustodian.Core + +Suite Setup Suite Initialization + +*** Tasks *** +Check for security hub findings in AWS account `${AWS_ACCOUNT_ID}` + [Documentation] Check for security hub findings + [Tags] aws security-hub + CloudCustodian.Core.Generate Policy + ... ${CURDIR}/security-hub.j2 + ... resource_providers=${AWS_RESOURCE_PROVIDERS} + ${total_count}= Set Variable 0 + FOR ${region} IN @{AWS_ENABLED_REGIONS} + ${c7n_output}= RW.CLI.Run Cli + ... cmd=custodian run -r ${region} --output-dir ${OUTPUT_DIR}/aws-c7n-security-hub/${region} ${CURDIR}/security-hub.yaml --cache-period 0 + ... secret__aws_access_key_id=${AWS_ACCESS_KEY_ID} + ... secret__aws_secret_access_key=${AWS_SECRET_ACCESS_KEY} + ... timeout_seconds=120 + ${dirs}= RW.CLI.Run Cli + ... cmd=find ${OUTPUT_DIR}/aws-c7n-security-hub/${region} -mindepth 1 -maxdepth 1 -type d | jq -R -s 'split("\n") | map(select(length > 0))'; + TRY + ${dir_list}= Evaluate json.loads(r'''${dirs.stdout}''') json + EXCEPT + Log Failed to load JSON payload, defaulting to empty list. WARN + END + + IF len(@{dir_list}) > 0 + FOR ${dir} IN @{dir_list} + ${count}= RW.CLI.Run Cli + ... cmd=cat ${dir}/metadata.json | jq '.metrics[] | select(.MetricName == "ResourceCount") | .Value'; + ${total_count}= Evaluate ${total_count} + int(${count.stdout.strip()}) + END + ELSE + Log No directories found to process. WARN + END + END + RW.Core.Push Metric ${total_count} + + +** Keywords *** +Suite Initialization + ${AWS_REGION}= RW.Core.Import User Variable AWS_REGION + ... type=string + ... description=AWS Region + ... pattern=\w* + ${AWS_ACCOUNT_ID}= RW.Core.Import User Variable AWS_ACCOUNT_ID + ... type=string + ... description=AWS Account ID + ... pattern=\w* + ${AWS_ACCESS_KEY_ID}= RW.Core.Import Secret AWS_ACCESS_KEY_ID + ... type=string + ... description=AWS Access Key ID + ... pattern=\w* + ${AWS_SECRET_ACCESS_KEY}= RW.Core.Import Secret AWS_SECRET_ACCESS_KEY + ... type=string + ... description=AWS Access Key Secret + ... pattern=\w* + ${AWS_RESOURCE_PROVIDERS}= RW.Core.Import User Variable AWS_RESOURCE_PROVIDERS + ... type=string + ... description=Comma separated list of AWS Resource Providers. + ... pattern=^[a-zA-Z0-9,]+$ + ... example="ec2,s3,rds,vpc" + ... default="ec2,s3,rds,vpc,ebs,iam-group,iam-policy,iam-role,iam-user" + ${clean_workding_dir}= RW.CLI.Run Cli cmd=rm -rf ${OUTPUT_DIR}/aws-c7n-security-hub + ${AWS_ENABLED_REGIONS}= RW.CLI.Run Cli + ... cmd=aws ec2 describe-regions --region ${AWS_REGION} --query 'Regions[*].RegionName' --output json + ... secret__aws_access_key_id=${AWS_ACCESS_KEY_ID} + ... secret__aws_secret_access_key=${AWS_SECRET_ACCESS_KEY} + ${AWS_ENABLED_REGIONS}= Evaluate json.loads(r'''${AWS_ENABLED_REGIONS.stdout}''') json + Set Suite Variable ${AWS_ENABLED_REGIONS} ${AWS_ENABLED_REGIONS} + Set Suite Variable ${AWS_REGION} ${AWS_REGION} + Set Suite Variable ${AWS_ACCOUNT_ID} ${AWS_ACCOUNT_ID} + Set Suite Variable ${AWS_ACCESS_KEY_ID} ${AWS_ACCESS_KEY_ID} + Set Suite Variable ${AWS_SECRET_ACCESS_KEY} ${AWS_SECRET_ACCESS_KEY} + Set Suite Variable ${AWS_RESOURCE_PROVIDERS} ${AWS_RESOURCE_PROVIDERS} \ No newline at end of file