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,22 @@
apiVersion: runwhen.com/v1
kind: GenerationRules
spec:
platform: azure
generationRules:
- resourceTypes:
- microsoft_network_network_security_groups
- microsoft_network_azure_firewalls
matchRules:
- type: pattern
pattern: ".+"
properties: ["name"]
mode: substring
slxs:
- baseName: azure-netsec-activity
levelOfDetail: basic
qualifiers: ["subscription_id", "resource_group"]
baseTemplateName: azure-network-security-activity-audit
outputItems:
- type: slx
- type: runbook
templateName: azure-network-security-activity-audit-taskset.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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/network/10062-icon-service-Virtual-Network.svg
alias: "{{match_resource.resource.name}} Azure NSG and Firewall Activity Audit"
asMeasuredBy: Activity Log mutations on network security and firewall resources versus allowlists.
configProvided:
- name: AZURE_SUBSCRIPTION_ID
value: "{{subscription_id}}"
- name: AZURE_RESOURCE_GROUP
value: "{{resource_group.name}}"
owners:
- {{workspace.owner_email}}
statement: Network security changes should be traceable to approved automation or known identities.
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
- name: scope
value: subscription
- name: access
value: read-only
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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: Audits Activity Log for NSG and Azure Firewall mutations and classifies callers for governance.
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-network-security-activity-audit/runbook.robot
configProvided:
- name: AZURE_SUBSCRIPTION_ID
value: "{{subscription_id}}"
- name: AZURE_RESOURCE_GROUP
value: "{{resource_group.name}}"
- name: ACTIVITY_LOOKBACK_HOURS
value: "168"
- name: CICD_APP_IDS
value: ""
- name: CICD_OBJECT_IDS
value: ""
- name: MAINTENANCE_START_HOUR_UTC
value: ""
- name: MAINTENANCE_END_HOUR_UTC
value: ""
- name: AZURE_TENANT_ID
value: ""
secretsProvided:
{% if wb_version %}
{% include "azure-auth.yaml" ignore missing %}
{% else %}
- name: azure_credentials
workspaceKey: AUTH DETAILS NOT FOUND
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Test infrastructure (Azure)

Optional Terraform in `terraform/` can provision an NSG and resource group for manual validation of this CodeBundle against a real subscription.

Create `terraform/tf.secret` (not committed) with:

```bash
export ARM_SUBSCRIPTION_ID="..."
export AZ_TENANT_ID="..."
export AZ_CLIENT_ID="..."
export AZ_CLIENT_SECRET="..."
```

Then run `task build-terraform-infra` from this directory after configuring credentials.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
version: "3"

tasks:
default:
desc: "Run config generation steps (requires tf.secret for full flow)"
cmds:
- task: validate-generation-rules

build-infra:
desc: "Build Terraform test resources"
cmds:
- task: build-terraform-infra

validate-generation-rules:
desc: "Validate YAML files in .runwhen/generation-rules"
cmds:
- |
for cmd in curl yq ajv; do
if ! command -v $cmd &> /dev/null; then
echo "Error: $cmd is required but not installed."
exit 1
fi
done
temp_dir=$(mktemp -d)
curl -s -o "$temp_dir/generation-rule-schema.json" \
https://raw.githubusercontent.com/runwhen-contrib/runwhen-local/refs/heads/main/src/generation-rule-schema.json
for yaml_file in ../.runwhen/generation-rules/*.yaml; do
echo "Validating $yaml_file"
json_file="$temp_dir/$(basename "${yaml_file%.*}.json")"
yq -o=json "$yaml_file" > "$json_file"
ajv validate -s "$temp_dir/generation-rule-schema.json" -d "$json_file" \
--spec=draft2020 --strict=false \
&& echo "$yaml_file is valid." || echo "$yaml_file is invalid."
done
rm -rf "$temp_dir"
silent: true

build-terraform-infra:
desc: "Run terraform apply"
dir: terraform
cmds:
- |
if [ ! -f tf.secret ]; then
echo "Create terraform/tf.secret with Azure credentials (see README.md)."
exit 1
fi
source tf.secret
export TF_VAR_subscription_id=$ARM_SUBSCRIPTION_ID
export TF_VAR_tenant_id=$AZ_TENANT_ID
terraform init
terraform apply -auto-approve || exit 1
silent: true

check-and-cleanup-terraform:
desc: "Destroy Terraform resources if state exists"
dir: terraform
cmds:
- |
if [ ! -f terraform.tfstate ]; then
echo "No state."
exit 0
fi
source tf.secret
export TF_VAR_subscription_id=$ARM_SUBSCRIPTION_ID
export TF_VAR_tenant_id=$AZ_TENANT_ID
terraform destroy -auto-approve
silent: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
tf.secret
.terraform/
terraform.tfstate
terraform.tfstate.backup
*.tfplan
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,34 @@
resource "azurerm_resource_group" "rg" {
name = var.resource_group
location = var.location
tags = var.tags
}

resource "azurerm_network_security_group" "test" {
name = "nsg-activity-audit-test"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
tags = var.tags
}

resource "azurerm_network_security_rule" "allow_ssh" {
name = "AllowSSH"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
network_security_group_name = azurerm_network_security_group.test.name
resource_group_name = azurerm_resource_group.rg.name
}

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

output "nsg_name" {
value = azurerm_network_security_group.test.name
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.18"
}
}
}

provider "azurerm" {
features {}
subscription_id = var.subscription_id
tenant_id = var.tenant_id
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource_group = "azure-netsec-activity-audit-test"
location = "East US"
tags = {
"env" = "test"
"lifecycle" = "deleteme"
"product" = "runwhen"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
variable "resource_group" {
type = string
}

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

variable "subscription_id" {
type = string
}

variable "tenant_id" {
type = string
}

variable "tags" {
type = map(string)
}
55 changes: 55 additions & 0 deletions codebundles/azure-network-security-activity-audit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Azure NSG and Firewall Change Activity Audit

This CodeBundle queries the Azure Activity Log for create, update, and delete operations on Network Security Groups (including rules), Azure Firewall, and firewall policy resources. It classifies callers against optional allowlists of CI/CD application IDs and managed identity object IDs, flags manual or out-of-band changes, and summarizes activity for governance and incident review.

## Overview

- **NSG mutations**: Write, delete, and action operations on `Microsoft.Network` NSG resources in the configured lookback window, including failed operations and high-volume warnings (Activity Log queries are capped per CLI request).
- **Firewall and policy mutations**: Similar coverage for Azure Firewall, firewall policies, rule collection groups, and related operations.
- **Caller classification**: Compares Activity Log claims (`appid`, object identifier, `caller`) to `CICD_APP_IDS` and `CICD_OBJECT_IDS` when set.
- **Governance flags**: Optional non-allowlisted identity issues and optional UTC maintenance window violations.
- **Summary**: Aggregates counts by operation and caller with a subscription Activity Log portal link.

Activity Log retention is typically 90 days at subscription scope; `az monitor activity-log list` returns at most `--max-events` records (default in scripts: 500) per query for the Microsoft.Network namespace filter.

## Configuration

### Required Variables

- `AZURE_SUBSCRIPTION_ID`: Subscription to audit.

### Optional Variables

- `AZURE_RESOURCE_GROUP`: Limit queries to this resource group; leave empty for subscription scope.
- `ACTIVITY_LOOKBACK_HOURS`: Hours of history to analyze (default: `168`).
- `CICD_APP_IDS`: Comma-separated Azure AD application (client) IDs approved for automation.
- `CICD_OBJECT_IDS`: Comma-separated object IDs for managed identities or service principals.
- `MAINTENANCE_START_HOUR_UTC`: Optional maintenance window start hour `0`–`23` UTC (use with `MAINTENANCE_END_HOUR_UTC`).
- `MAINTENANCE_END_HOUR_UTC`: Optional end hour; window is `[start, end)` when start is less than end, otherwise wraps midnight.
- `AZURE_TENANT_ID`: Optional tenant ID included in the summary JSON for portal context.

### Secrets

- `azure_credentials`: Service principal JSON or environment fields (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET`). Reader on the subscription is sufficient for Activity Log read.

## Tasks Overview

### Query Activity Log for NSG Mutations

Lists NSG-related write/delete/action events, records raw JSON to `nsg_writes_raw.json`, and emits issues for failed operations or unusually high volume relative to the query cap.

### Query Activity Log for Azure Firewall and Policy Mutations

Same pattern for Azure Firewall and firewall policy resources; output in `firewall_writes_raw.json` and `firewall_issues.json`.

### Classify Callers Against Allowlist

Merges NSG and firewall events and tags each as automated or manual/unknown. If allowlists are empty, emits an informational issue to configure `CICD_APP_IDS` / `CICD_OBJECT_IDS`.

### Flag Manual or Out-of-Band Changes

Raises higher-severity issues for identities not on the allowlist (when allowlists are configured) and for mutations outside the optional UTC maintenance window.

### Summarize Change Timeline and Top Actors

Produces `summary_report.json` with counts by operation and caller plus an Activity Log blade URL for the subscription.
Loading