Monitoring Audit Logs in Slack with Event Hubs + Azure Functions

Posted by Steven Follis on May 19, 2016

Colors

Keeping tabs on an Azure Subscription can be difficult. What’s happening in our environments? Are operations failing? These questions are hard enough with a handful of resources, but in a busy subscription containing hundreds of resources we can quickly be searching for needles in a haystack.

Here’s where logs can help. Azure Audit Logs give us a consistent way of tracking Operations and Events at the Resource, Resource Group, or Subscription level.

Azure Audit Logs

These logs get down in the nitty gritty of our resources and serve as a critical tool in tracking changes to our environments over time. Logs are kept for 90 days, which can be easily extended by configuring continuous export to an Azure Storage Account. This makes a great use case for the brand new Azure Cool Blob Storage accounts (I love that new resource smell!)

Keeping our logs sitting around until the next ice age is great, but what if I want actionable data right now? For that we can turn to a new feature of Audit Logs: Export to Azure Event Hub. This new capability allows us to pipe log data into an Event Hub, which is optimized for high levels of data ingestion. Once in the hub, we can interact with that data in a variety of ways.

For example, we could use Azure Stream Analytics to analyze the data pouring into the hub by taking advantage of its SQL-esque syntax and windowing support. Or we could get our big data on by processing Event Hub data with Storm. We could roll a Worker Role with the lovable EventProcessorHost.

Those are all great options, but sound like a lot of work. All I want to do is pop an announcement into Slack whenever there’s an event that I need to take a look at my subscription. Valuing speed, I decided to take the new Azure Functions out for a spin.

(Shout out to my boy colleague @romitgirdhar whose cube used to be near mine in Charlotte)

Every time I touch Functions I smile like a kid that just got handed a puppy. As a developer, Functions abstract away as much boilerplate, table stakes code as possible, and allows me to focus on code that I actually care about. It is delightful, and has support for C#, Node.js, Python, F#, PHP, batch, bash, Java, and a partridge in a pear tree.

Configure Slack

Let’s start by getting Slack ready to go. We’ll need to do three things:

  • Create a new channel that will be dedicated to our alerts
  • Setup a web hook integration that will pipe messages into our channel
  • Connect the web hook to our channel

Inside of Slack, create a new channel named “auditlogs” by clicking the “+” icon next to “Channels” on the left hand nav. From there, select the gear icon at the top and select “Add an app or integration”

Slack start

This will pop a browser window, where you can search for “Incoming WebHooks”. This integration sets up an endpoint inside of Slack. Whenever an HTTP post hits that endpoint carrying a message, the message will be inserted into a Slack channel. Web Hooks are a terrific way to integrate disparate systems in a straightforward manner.

Slack Integrations

Follow the wizard to stand up a web hook integration for the “auditlogs” channel, being sure to make note of the “Webhook URL”. We will need that shortly.

Webhook Settings

Taking the URL and the sample from the “Sending Messages” section’s documentation, let’s test that endpoint in Postman to see webhook functionality in action.

Postman test

Postman Success

When we send a simple HTTP POST to the endpoint, the object we pass it beautifully appears in our channel. Now that have a Slack integration ready to accept POSTs, let’s fire up Azure.

Azure

First off, we need a Service Bus Namespace. This will serve as a container for our Event Hub. From the Azure Portal, hit the “+” sign in the top right, and under “Hybrid Integration” select “Service Bus”.

Azure Portal Service Bus

Currently, the Service Bus functionality has not yet landed in the new portal, so we find ourselves quickly sitting in the Classic Portal. What we are really after is the Service Bus Namespace, so do not worry too much about the Queue name (we’re about to delete it).

Service Bus Creation

After the Service Bus is provisioned, click into the new resource and delete the queue.

Service Bus Home Delete Queue

Back in the Ibiza Portal, let’s hook up the Service Bus to our Audit Logs. Select the flyout from the left nav and search for “Audit” to quickly locate the link for the Audit Log blad. Click it to have the blade flyout.

Portal Search

Next, click the “Export” button from the top nav to show the export options.

Export button

Click on the section for “Azure Event Hub” and select our Service Bus. This export functionality will automatically setup an Event Hub for us, hence why we only configured a Service Bus Namespace without an actual Event Hub earlier.

EH Export

However, after clicking “OK” the “Export Audit Logs” blade won’t let us save. It wants us to configure a storage account first. I’m not sure if this is a UI bug (it says right at the top this is “Preview”) or intended behavior, but it creates a dead end.

greyed out options

I went ahead and created a new storage account (Standard_LRS), and piped it into this configuration area. Also worth mentioning that I selected all the regions available in the previous selection drop down.

Option Available

With the export functionality wired up, we now need a Function to handle our logic. Provision a new Function in the portal with the little green “+” sign in the top left, putting it in the same Resource Group as the Storage account we created previously.

Provision Function RG Resources

Opening up our Function, let’s start with “New” in the top left. Selecting “Node.js” and “Core” from the dropdowns, click the tile for “EventHubTrigger - Node”. This template will get us started with an input binding for Event Hubs. Input and output bindings mean we don’t have to write all the code to hook into and transfer data between a data source - in this case, an Event Hub. It’s magic.

New Function Settings

Scrolling down we can configure the input binding. Remember when I said the Audit Log Export feature creates an event hub for us? It’s named “insights-operational-logs”. Fill that in, and click “Select” next to the Event Hub Connection box.

Binding Config

Here’s where we connect back to our Event Hub.

SB Config

Only problem is that we haven’t yet setup a Shared Access Policy. We need a SAP to get a connection string. Sorry to keep jumping between portals, but open Classic Portal in a new tab. From there, open our Event Hub and click the “Configure” tab. Here we can create a new Shared Access Policy with “Listen” Permissions. This permission level allows our Function to read items off of the Event Hub. Click save, then head to the Dashboard tab on the top nav.

EH Policy Setup

On the dashboard of our Event Hub, click “Connection Strings” on the bottom nav bar and copy the String for the policy name you just created.

EH String

Back in the new portal’s tab, use the connection string to complete the “Add a Service Bus Connection” blade. Before pasting, remove the ;EntityPath=insights-operational-logs from the end of the string.

Endpoint=sb://auditlogprocessor-ns.servicebus.windows.net/;SharedAccessKeyName=FunctionPolicy;SharedAccessKey=Fc1et/idbtoprwO5K1MLaFgegXTuXpdR/eUW2Dm3swc=;EntityPath=insights-operational-logs

becomes

Endpoint=sb://auditlogprocessor-ns.servicebus.windows.net/;SharedAccessKeyName=FunctionPolicy;SharedAccessKey=Fc1et/idbtoprwO5K1MLaFgegXTuXpdR/eUW2Dm3swc=

The entity is handled in “Event Hub Name” box in the function binding configuration. Click on and then “Create” the function.

SB Connection

Function Setup Finished

We now have a Function App ready for us. You may see a flurry of activity as your Function comes online and processes the logs that have been piped from the Audit Logs into the Event Hub. These logs have been sitting in the Event Hub just waiting for a consumer (our Function) to come pick them up and process them.

Console catchup

Now for our custom code. We want to do a variety of things

  • Loop through each received log entry, as the Event Hub sends us an array of entries
  • Discard any log entries that are Informational in nature. There are tons of these generated by the Azure service, and they do not typically require immediate attention
  • Build a response packet of formatted strings
  • Send response to the Slack Web Hook endpoint

For these activities, we use the following code:

"use strict";

var request = require('request');

module.exports = function (context, myEventHubTrigger) {

    context.log('======================================================');
    context.log('Node.js eventhub trigger function processed work item');

    // Grab logs from the returned records object
    var logs = myEventHubTrigger.records;

    // Prepare a response
    // Multiple logs may be received, but we'll batch into one response 
    var alert = [];

    // Loop through the logs
    var counter = 0;
    logs.forEach(function (log) {

        // Only process logs that are non-informational or a new RG
        // This cuts down on chatter everytime Azure ties its shoes
        if (log.level !== 'Information' || log.operationName === 'MICROSOFT.RESOURCES/SUBSCRIPTIONS/RESOURCEGROUPS/WRITE') {

            // Build out an alert's markup
            alert.push(buildAlert(log));

        }

        // Iterate counter and check progress
        counter++;
        if (counter === logs.length) {

            context.log('Finished looping through logs');

            // Finished iterating, send batched alert
            sendAlert(context, alert);

        }

    });

};

function buildAlert(log) {

    // String build via an array
    // Using ES6 Template Literals 
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
    var logContent = [
        `time:\t ${log.time}`,
        `tenantId:\t ${log.identity.claims["http://schemas.microsoft.com/identity/claims/tenantid"]}`,
        `operationName:\t ${log.operationName}`,
        `category:\t ${log.category}`,
        `resultType:\t ${log.resultType}`,
        `resultSignature:\t ${log.resultSignature}`,
        `identity:\t ${log.identity.claims.name} (${log.identity.claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"]})`,
        `location:\t ${log.location}`,
        `resourceId: ${log.resourceId}`
    ];

    // Determine color for alert
    var color;
    switch (log.level) {
        case 'Error':
            color = 'danger';
            break;
        case 'Warning':
            color = 'warning';
            break;
        case 'Information':
            color = '#439FE0';
            break;
    }

    // Create object to return
    var formattedLog = {
        "fallback": logContent.join('\n'),
        "color": color,
        "fields": [
            {
                "title": log.level,
                "value": logContent.join('\n'),
                "short": false
            }
        ]
    };

    // Return
    return formattedLog;

}

function sendAlert(context, alert) {

    // Send alert to Slack webhook
    request.post(process.env.SLACK_WEBHOOK_URL, {
        json: {
            "username": "audit-bot",
            "icon_url": "https://pbs.twimg.com/profile_images/546024114272468993/W9gT7hZo.png",
            "attachments": alert
        }
    }, function (error) {

        if (error) {
            context.log('Error posting the message');
            context.done(error);
        }
        else {
            context.log('Successfully posted the message');
            context.done();
        }

    });

}

Our code needs a few additional last pieces to function. In sendAlert() we’re using an environmental variable rather than hardcoding the URL directly into our code. This makes the function more flexible.

To set that environmental variable, click “Function App Settings” in the top right corner of the Function designer and select “Go to App Service Settings”.

App Settings Blade

Oh hello there, App Service Blade! You look familiar! From here, we can head to Settings -> Application Settings to add in an environmental variable named SLACK_WEBHOOK_URL, just like any other Azure Web App.

Function App Settings

Also note that there’s an App Setting called “EventHubConnection”. That should also look familiar, as it’s the connection we previously configured in the Function’s binding configuration. Save the Application S

One more comment on the code from above is that we have a dependency on a NPM library named “Request”. We need to go add this library into our Function, or else the code will fail. On the main App Service Blade, click “Browse” on the top nav to pop a new tab navigating to our Function app’s URL.

Spoiler: it’s going to be anti-climactic. We don’t have pages to serve back, so you all you’ll see is a lovely white screen. Instead, let’s head over to the Kudu add .scm into the URL structure right before .azurewebsites.net. Our original URL of http://auditlogfunction.azurewebsites.net/ becomes http://auditlogfunction.scm.azurewebsites.net/ and voila! Kudu Time™.

Kudu Home

Click the “Debug Console” from the top nav and select PowerShell. From here we can navigate to our Function app by selecting site -> wwwroot -> AuditLogProcessor (our function name). In this directory we can see index.js containing the code we pasted in earlier, and function.json containing the bindings for our Function.

Click into the console, type in npm install request and hit enter. The package will be installed and our app is ready to rock.

Console

We could have provided an entire package.json chalked full of NPM dependencies, but either way we get a lovely node_modules folder.

NPM modules

Wrap Up

We now have an Azure Function that pulls messages off of an Event Hub, which is receiving a steady stream of data from the Azure Audit Logs. The Function then hits a Slack WebHook to alert me that I need to take action. Slack even gives us pretty colors to use in distinguishing Informational logs from Errors or Warnings.

Colors

Enjoy!

(Oh and that was a ton of steps. Take the easy way and download this ARM Template that deploys all the above infrastructure AND does a Web Deploy of the Function code. I won’t judge.)