Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: runwhen.com/v1
kind: GenerationRules
spec:
platform: azure
generationRules:
- resourceTypes:
- azure_network_security_groups
matchRules:
- type: pattern
pattern: ".+"
properties: [name]
mode: substring
slxs:
- baseName: azure-nsg-desired-state-drift
qualifiers: ["resource", "resource_group", "subscription_id"]
baseTemplateName: azure-nsg-desired-state-drift
levelOfDetail: basic
outputItems:
- type: slx
- type: runbook
templateName: azure-nsg-desired-state-drift-taskset.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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/azure/networking/10062-icon-service-Load-Balancers.svg
alias: "{{match_resource.resource.name}} Azure NSG Desired-State Drift"
asMeasuredBy: Drift between live NSG rules or associations and the declared baseline for this network security group.
configProvided:
- name: SLX_PLACEHOLDER
value: SLX_PLACEHOLDER
owners:
- {{workspace.owner_email}}
statement: NSG {{match_resource.resource.name}} should match the repository baseline with no unauthorized rule or attachment changes.
additionalContext:
{% include "azure-hierarchy.yaml" ignore missing %}
qualified_name: "{{ match_resource.qualified_name }}"
tags:
{% include "azure-tags.yaml" ignore missing %}
- name: cloud
value: azure
- name: service
value: network-security-groups
- name: scope
value: resource
- name: access
value: read-only
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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: Compare live NSG rules and associations for {{ match_resource.resource.name }} against a declared baseline in resource group {{ resource_group.name }}.
codeBundle:
{% if repo_url %}
repoUrl: {{repo_url}}
{% else %}
repoUrl: https://github.com/runwhen-contrib/rw-cli-codecollection.git
{% endif %}
{% if ref %}
ref: {{ref}}
{% else %}
ref: main
{% endif %}
pathToRobot: codebundles/azure-nsg-desired-state-drift/runbook.robot
configProvided:
- name: AZURE_SUBSCRIPTION_ID
value: "{{subscription_id}}"
- name: AZURE_RESOURCE_GROUP
value: "{{ resource_group.name }}"
- name: NSG_NAME
value: "{{match_resource.resource.name}}"
- name: NSG_NAMES
value: "{{match_resource.resource.name}}"
- name: BASELINE_PATH
value: "{{custom.nsg_baseline_path | default('PLACEHOLDER_BASELINE_PATH')}}"
- name: BASELINE_FORMAT
value: "{{custom.nsg_baseline_format | default('json-bundle')}}"
- name: ASSOCIATION_BASELINE_PATH
value: "{{custom.nsg_association_baseline_path | default('')}}"
- name: COMPARE_DEFAULT_RULES
value: "{{custom.compare_default_rules | default('false')}}"
- name: IGNORE_RULE_PREFIXES
value: "{{custom.ignore_rule_prefixes | default('')}}"
- name: REQUIRE_ASSOCIATIONS
value: "{{custom.require_associations | default('false')}}"
secretsProvided:
{% if wb_version %}
{% include "azure-auth.yaml" ignore missing %}
{% else %}
- name: azure_credentials
workspaceKey: AUTH DETAILS NOT FOUND
{% endif %}
28 changes: 28 additions & 0 deletions codebundles/azure-nsg-desired-state-drift/.test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Test infrastructure — Azure NSG desired-state drift

Terraform provisions a resource group and a sample NSG with one inbound rule for manual validation of this CodeBundle in a real Azure subscription.

## Prerequisites

- Azure CLI and Terraform installed
- `terraform/tf.secret` (not committed) with `ARM_SUBSCRIPTION_ID`, `AZ_TENANT_ID`, `AZ_CLIENT_ID`, `AZ_CLIENT_SECRET` per `docs/skills/test-infra-azure.md`

## Usage

```bash
task build-terraform-infra
```

After apply, capture a baseline with:

```bash
az network nsg show -g <rg> -n <nsg> -o json > /tmp/nsg.json
```

Build a `json-bundle` file with a top-level `nsgs` array containing that object (or run the CodeBundle export task and save `nsg_live_bundle.json`). Point `BASELINE_PATH` at that file for drift testing.

```bash
task cleanup-terraform-infra
```

See `Taskfile.yaml` for `validate-generation-rules` and other tasks copied from the standard Azure CodeBundle test layout.
122 changes: 122 additions & 0 deletions codebundles/azure-nsg-desired-state-drift/.test/Taskfile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
version: "3"

tasks:
default:
desc: "Run/refresh config"
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 for uncommitted/unpushed changes before testing.
vars:
BASE_DIR: "../"
cmds:
- |
UNCOMMITTED=$(git diff --name-only HEAD | grep -E "^${BASE_DIR}" | grep -v "/\.test/" || true)
if [ -n "$UNCOMMITTED" ]; then
echo "Uncommitted changes found. Commit and push before testing."
exit 1
fi
silent: true

generate-rwl-config:
desc: "Generate RunWhen Local workspaceInfo.yaml"
env:
ARM_SUBSCRIPTION_ID: "{{.ARM_SUBSCRIPTION_ID}}"
AZ_TENANT_ID: "{{.AZ_TENANT_ID}}"
AZ_CLIENT_SECRET: "{{.AZ_CLIENT_SECRET}}"
AZ_CLIENT_ID: "{{.AZ_CLIENT_ID}}"
RW_WORKSPACE: '{{.RW_WORKSPACE | default "my-workspace"}}'
cmds:
- |
source terraform/tf.secret
repo_url=$(git config --get remote.origin.url)
branch_name=$(git rev-parse --abbrev-ref HEAD)
codebundle=$(basename "$(dirname "$PWD")")
pushd terraform > /dev/null
resource_group=$(terraform show -json terraform.tfstate 2>/dev/null | jq -r '.values.root_module.resources[]? | select(.type == "azurerm_resource_group") | .values.name' | head -1)
popd > /dev/null
if [ -z "$resource_group" ]; then
echo "Apply Terraform first to obtain resource group name."
exit 1
fi
cat <<EOF > workspaceInfo.yaml
workspaceName: "$RW_WORKSPACE"
workspaceOwnerEmail: authors@runwhen.com
defaultLocation: location-01-us-west1
defaultLOD: detailed
cloudConfig:
azure:
subscriptionId: "$ARM_SUBSCRIPTION_ID"
tenantId: "$AZ_TENANT_ID"
clientId: "$AZ_CLIENT_ID"
clientSecret: "$AZ_CLIENT_SECRET"
resourceGroupLevelOfDetails:
$resource_group: detailed
codeCollections:
- repoURL: "$repo_url"
branch: "$branch_name"
codeBundles: ["$codebundle"]
EOF
silent: true

run-rwl-discovery:
desc: "Run RunWhen Local discovery"
cmds:
- echo "See codebundle-farm docs/skills/test-infra-azure.md for docker-based discovery."

validate-generation-rules:
desc: "Validate .runwhen/generation-rules YAML"
cmds:
- |
for yaml_file in ../.runwhen/generation-rules/*.yaml; do
echo "Checking $yaml_file"
python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" "$yaml_file" || exit 1
done
echo "OK"

build-terraform-infra:
desc: "Terraform apply"
dir: terraform
cmds:
- |
source tf.secret
terraform init
terraform apply -auto-approve

cleanup-terraform-infra:
desc: "Terraform destroy"
dir: terraform
cmds:
- |
source tf.secret
terraform destroy -auto-approve

check-terraform-infra:
desc: "List terraform state if present"
dir: terraform
cmds:
- terraform state list 2>/dev/null || echo "No state"

check-and-cleanup-terraform:
desc: "Destroy if state exists"
cmds:
- task: cleanup-terraform-infra

clean-rwl-discovery:
desc: "Remove local discovery artifacts"
cmds:
- rm -f workspaceInfo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
terraform {
backend "local" {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
data "azurerm_client_config" "current" {}

resource "random_id" "suffix" {
byte_length = 2
}

resource "azurerm_resource_group" "rg" {
name = "${var.resource_group}-${random_id.suffix.hex}"
location = var.location
tags = var.tags
}

resource "azurerm_network_security_group" "nsg" {
name = "rwtest-nsg-${random_id.suffix.hex}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
tags = var.tags

security_rule {
name = "AllowSSH"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}

output "resource_group_name" {
value = azurerm_resource_group.rg.name
}

output "nsg_name" {
value = azurerm_network_security_group.nsg.name
}

output "subscription_id" {
value = data.azurerm_client_config.current.subscription_id
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "4.18.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}

provider "azurerm" {
features {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource_group = "rw-nsg-drift-test"
location = "East US"
tags = {
"env" = "test"
"lifecycle" = "deleteme"
"product" = "runwhen"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
variable "resource_group" {
type = string
description = "Base name for the test resource group"
}

variable "location" {
type = string
default = "East US"
}

variable "tags" {
type = map(string)
}
Loading