ChangeVault
Integrations

Azure Activity Log

Automatically capture every resource change in your Azure subscription and forward it to ChangeVault.

Overview

Azure Activity Log records every control-plane operation in your subscription — resources being created, modified, deleted, role assignments changed, and more. By connecting it to ChangeVault you get a complete, automatic audit trail without anyone needing to manually log a change.

Architecture

Azure Subscription (Activity Log)
        ↓  Diagnostic Settings
   Azure Event Hub
        ↓  trigger
  Azure Function
        ↓  POST /api/changes
  ChangeVault

What gets captured

Operation typeExampleRisk level
Resource createdNew VM, storage account, databaseMedium
Resource updatedVM resized, firewall rule changedMedium
Resource deletedVM deleted, subnet removedHigh
ActionStart/stop VM, rotate keyMedium
Failed operationDeployment failedRolled back

Read-only operations (/read) and "Started" events (before completion) are filtered out.


Prerequisites

  • An Azure subscription with Owner or Contributor access
  • Azure CLI installed, or access to the Azure Portal
  • Node.js 18+ (for the Azure Function)

Step 1 — Create an Event Hub

# Create resource group (skip if you have one)
az group create --name changevault-rg --location westeurope

# Create Event Hub namespace
az eventhubs namespace create \
  --name changevault-eventhub \
  --resource-group changevault-rg \
  --sku Standard

# Create the event hub
az eventhubs eventhub create \
  --name insights-operational-logs \
  --namespace-name changevault-eventhub \
  --resource-group changevault-rg \
  --partition-count 4 \
  --retention-time 1

# Create a consumer group for the Function
az eventhubs eventhub consumer-group create \
  --name changevault \
  --eventhub-name insights-operational-logs \
  --namespace-name changevault-eventhub \
  --resource-group changevault-rg

Step 2 — Enable Diagnostic Settings

Export the Activity Log to the Event Hub. Replace <subscription-id>, <rg> and <ns> with your values.

az monitor diagnostic-settings create \
  --name "changevault-export" \
  --resource /subscriptions/<subscription-id> \
  --event-hub-rule /subscriptions/<subscription-id>/resourceGroups/changevault-rg/providers/Microsoft.EventHub/namespaces/changevault-eventhub/authorizationRules/RootManageSharedAccessKey \
  --event-hub "insights-operational-logs" \
  --logs '[
    {"category": "Administrative", "enabled": true},
    {"category": "Security",       "enabled": true},
    {"category": "Policy",         "enabled": true}
  ]'

You can also do this in the Portal under Monitor → Activity log → Export Activity Logs → Add diagnostic setting.


Step 3 — Deploy the Azure Function

Create the Function App

az functionapp create \
  --name changevault-forwarder \
  --resource-group changevault-rg \
  --storage-account <your-storage-account> \
  --consumption-plan-location westeurope \
  --runtime node \
  --runtime-version 20 \
  --functions-version 4

Set environment variables

# Get the Event Hub connection string
EVENTHUB_CONN=$(az eventhubs namespace authorization-rule keys list \
  --resource-group changevault-rg \
  --namespace-name changevault-eventhub \
  --name RootManageSharedAccessKey \
  --query primaryConnectionString -o tsv)

az functionapp config appsettings set \
  --name changevault-forwarder \
  --resource-group changevault-rg \
  --settings \
    "EventHubConnectionString=$EVENTHUB_CONN" \
    "CHANGEVAULT_API_KEY=cvk_prod_••••••••" \
    "CHANGEVAULT_URL=https://app.changevault.dev"

Function code — src/functions/index.js

const { app } = require("@azure/functions");

const API_KEY = process.env.CHANGEVAULT_API_KEY;
const API_URL = process.env.CHANGEVAULT_URL ?? "https://app.changevault.dev";

function shouldForward(record) {
  if (record.category !== "Administrative") return false;
  if (record.resultType === "Start") return false; // skip "started", only log completions
  const op = (record.operationName ?? "").toLowerCase();
  return op.endsWith("/write") || op.endsWith("/delete") || op.endsWith("/action");
}

function toChange(record) {
  const op = record.operationName ?? "";
  const resourceName = (record.resourceId ?? "").split("/").pop() ?? "unknown";
  const resourceType = op.split("/").slice(0, 2).join("/");
  const isDelete = op.toLowerCase().endsWith("/delete");

  return {
    title: `Azure: ${isDelete ? "Deleted" : "Updated"} ${resourceType} — ${resourceName}`,
    description: `Caller: ${record.caller ?? "unknown"}\nOperation: ${op}\nResource: ${record.resourceId}`,
    risk_level: isDelete ? "high" : "medium",
    status: record.resultType === "Failed" ? "rolled_back" : "completed",
    tags: ["azure", "activity-log", resourceType.toLowerCase().replace(/[^a-z0-9-]/g, "-")],
  };
}

app.eventHub("activityLogForwarder", {
  connection: "EventHubConnectionString",
  eventHubName: "insights-operational-logs",
  consumerGroup: "changevault",
  cardinality: "many",
  handler: async (messages) => {
    const all = Array.isArray(messages) ? messages : [messages];
    const records = all.flatMap((m) => m.records ?? [m]);

    await Promise.all(
      records.filter(shouldForward).map((record) =>
        fetch(`${API_URL}/api/changes`, {
          method: "POST",
          headers: {
            "x-api-key": API_KEY,
            "Content-Type": "application/json",
          },
          body: JSON.stringify(toChange(record)),
        })
      )
    );
  },
});

package.json

{
  "name": "changevault-forwarder",
  "version": "1.0.0",
  "main": "src/functions/index.js",
  "dependencies": {
    "@azure/functions": "^4.0.0"
  }
}

Deploy

cd changevault-forwarder
npm install
func azure functionapp publish changevault-forwarder

Filtering specific operations

To limit which operations get forwarded, modify the shouldForward function. For example, to only log compute and network changes:

const ALLOWED_PROVIDERS = ["microsoft.compute", "microsoft.network", "microsoft.sql"];

function shouldForward(record) {
  if (record.category !== "Administrative") return false;
  if (record.resultType === "Start") return false;
  const op = (record.operationName ?? "").toLowerCase();
  if (!op.endsWith("/write") && !op.endsWith("/delete")) return false;
  return ALLOWED_PROVIDERS.some((p) => op.startsWith(p));
}

Estimated cost

ResourceEstimated monthly cost
Event Hub (Standard, 1 TU)~€9
Azure Function (Consumption plan)Free for first 1M executions
Storage account< €1

For most subscriptions the total cost is under €10/month.

API access requires the Pro plan or higher.

On this page