Debug School

Cover image for NetScout Graylog config
Suyash Sambhare
Suyash Sambhare

Posted on

NetScout Graylog config

This code is a data transformation script designed to process audit event objects—likely from a system like Bitbucket—and flatten, clean, and normalize the data for easier downstream consumption (e.g., indexing in Elasticsearch via Filebeat). Here's a breakdown of what it does:

Summary

This script:

  • Normalizes audit data for indexing.
  • Flattens nested arrays into key-value pairs.
  • Cleans up irrelevant metadata.
  • Formats timestamps and adds human-readable summaries.

It’s tailored for use with Filebeat and feeds into an Graylog for audit logging and analysis.

FileBeat Cloud

# Example framework of how you might use Filebeat with Atlassian Guard (adjust as necessary!)
#
# Set these via Sidecar variables (or edit file to remove variables and hardcode values)
#
# ${ORG_ID} e.g. 1234abcd-...
# ${ORG_API_KEY} Atlassian Organization API key
# ${GRAYLOG_HOST} e.g. graylog.example.com
# ${BEATS_PORT} e.g. 5044

filebeat.inputs:
  - type: httpjson
    id: atlassian_guard_stream
    interval: 60s
    request:
      url: https://api.atlassian.com/admin/v1/orgs/${ORG_ID}/events-stream
      method: GET
      rate_limit:
        limit: 10
        burst: 1
        period: 1m
      retry:
        max_attempts: 5
        wait_min: 2s
        wait_max: 60s
      transforms:
        - set:
            target: header.Authorization
            value: 'Bearer ${ORG_API_KEY}' # Guard uses API key as Bearer
        - set: { target: header.Accept, value: "application/json" }
        - set: { target: url.params.limit, value: "50" }
        - set: { target: url.params.cursor,value: "[[.cursor.next]]" }
      response: # 1) split into one event per item in data[]
        split:
          target: body.data
          type: array
          keep_parent: true # keep meta.next on the event for debugging
          ignore_empty_value: true # persist meta.next so next poll starts where we left off
      cursor:
        next:
          value: '[[.last_response.body.meta.next]]'
          ignore_empty_value: true # can be helpful while testing – writes request/response bodies to a file
          # request.tracer.enabled: true
          # request.tracer.filename: /tmp/guard_http_trace.json

processors:
  - script:
      lang: javascript
      id: guard_parse_and_flatten
      source: >-
        (insert transformation script here - see below)
 # if we didn't extract an action, drop the event
  - drop_event:
      when:
        not:
          has_fields: ["guard.action","message"]

# Send to Graylog Beats input
output.logstash:
  hosts: ["${GRAYLOG_HOST}:${BEATS_PORT}"]
Enter fullscreen mode Exit fullscreen mode

Sample Transformation script in JavaScript

function process(event) {
    event.Put("event_received_time", new Date());

    function toCamelCase(str) {
        return str.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, function(match, chr) {
            return chr.toUpperCase();
        });
    }
    (event.Get("audit.affectedObjects") || []).forEach(function(o) {
        if (o.type && o.name) {
            event.Put("audit.affectedObject." + toCamelCase(o.type), o.name);
        }
    });
    event.Delete("audit.affectedObjects");
    (event.Get("audit.extraAttributes") || []).forEach(function(a) {
        if (a.name && a.value && a.name !== "Load balancer/proxy IP address") {
            event.Put("audit.extraAttribute." + toCamelCase(a.name), a.value);
        }
    });
    event.Delete("audit.extraAttributes");
    (event.Get("audit.changedValues") || []).forEach(function(c) {
        if (!c.key) {
            return;
        }
        var k = "audit.changedValue." + toCamelCase(c.key);
        var d = c.from && c.to ? (c.from + "" + c.to) : (c.to ||
            (c.from ? "from " + c.from : ""));
        if (d) {
            event.Put(k, d);
        }
    });
    event.Delete("audit.changedValues");
    var ms = Number(event.Get("audit.timestamp.epochSecond") || 0) * 1000 + Math.floor(Number(event.Get("audit.timestamp.nano") || 0) / 1e6);
    event.Put("@timestamp", new Date(ms));
    event.Delete("audit.timestamp");
    var author = event.Get("audit.author.name") || "";
    event.Put("user_name", author);
    event.Put("message",
        ((event.Get("audit.auditType.action") || "") + (author && author !== "Anonymous" ? " by " + author : "")));
    // Clean up i18n keys and other unwanted fields
    event.Delete("audit.auditType.actionI18nKey");
    event.Delete("audit.auditType.categoryI18nKey");
    event.Delete("audit.author.uri");
    return event;
}
// Run 'filebeat -c filebeat.yml test config' to make sure script code is functioning as expected
function test() {
    var sampleEvent = {
        "audit": {
            "auditType": {
                "action": "Pull request participants changed",
                "actionI18nKey": "bitbucket.service.pullrequest.audit.action.pullrequestparticipantsupdatedevent",
                "area": "END_USER_ACTIVITY",
                "category": "Pull requests",
                "categoryI18nKey": "bitbucket.service.audit.category.pullrequests",
                "level": "BASE"
            },
            "affectedObjects": [{
                    "id": "1234",
                    "name": "PR#123",
                    "type": "PULL_REQUEST"
                },
                {
                    "id": "5678",
                    "name": "testrepo",
                    "type": "REPOSITORY"
                },
                {
                    "id": "3445",
                    "name": "PROJ",
                    "type": "PROJECT"
                }
            ],
            "changedValues": [{
                "i18nKey": "bitbucket.service.pullrequest.audit.changedvalue.pullrequestparticipantsupdated.participants",
                "key": "Participants",
                "to": "bitbucket.system-user"
            }],
            "extraAttributes": [{
                    "name": "From ref",
                    "nameI18nKey": "bitbucket.service.pullrequest.audit.attribute.pullrequest.fromref",
                    "value": "test-branch"
                },
                {
                    "name": "To ref",
                    "nameI18nKey": "bitbucket.service.pullrequest.audit.attribute.pullrequest.toref",
                    "value": "main"
                },
                {
                    "name": "Load balancer/proxy IP address",
                    "value": "10.1.2.3"
                }
            ],
            "author": {
                "id": "-1",
                "name": "System",
                "type": "system"
            },
            "timestamp": {
                "epochSecond": 1755786989,
                "nano": 582000000
            }
        }
    };
    var event = process(new Event(sampleEvent));
    // Test that i18n keys are removed
    if (event.Get("audit.auditType.actionI18nKey") !== null) {
        throw "expected auditType.actionI18nKey to be removed";
    }
    if (event.Get("audit.auditType.categoryI18nKey") !== null) {
        throw "expected auditType.categoryI18nKey to be removed";
    }
    // Test that flattened fields are created
    if (event.Get("audit.affectedObject.pullRequest") !== "PR#123") {
        throw "expected audit.affectedObject.pullRequest to be 'PR#123'";
    }
    if (event.Get("audit.affectedObject.repository") !== "testrepo") {
        throw "expected audit.affectedObject.repository to be 'testrepo'";
    }
    if (event.Get("audit.affectedObject.project") !== "PROJ") {
        throw "expected audit.affectedObject.project to be 'PROJ'";
    }
    if (event.Get("audit.affectedObject.project") !== "PROJ") {
        throw "expected audit.affectedObject.project to be 'PROJ'";
    }
    if (event.Get("audit.changedValue.participants") !== "bitbucket.system-user") {
        throw "expected audit.changedValue.participants to be 'bitbucket.system-user'";
    }
    if (event.Get("audit.extraAttribute.fromRef") !== "test-branch") {
        throw "expected audit.extraAttribute.fromRef to be 'test-branch'";
    }
    if (event.Get("audit.extraAttribute.toRef") !== "main") {
        throw "expected audit.extraAttribute.toRef to be 'main'";
    }
    // Test that Load balancer IP is excluded
    if (event.Get("audit.extraAttribute.loadBalancerProxyIpAddress") !== null) {
        throw "expected Load balancer IP to be excluded";
    }
    // Test that original arrays are deleted (we have individual fields now)
    if (event.Get("audit.affectedObjects") !== null) {
        throw "expected audit.affectedObjects to be deleted";
    }
    if (event.Get("audit.changedValues") !== null) {
        throw "expected audit.changedValues to be deleted";
    }
    if (event.Get("audit.extraAttributes") !== null) {
        throw "expected audit.extraAttributes to be deleted";
    }
}
Enter fullscreen mode Exit fullscreen mode

Graylog

FileBeat Datacenter

This below part processes a structured audit event object—likely from a system like Bitbucket—and transforms it into a flattened, cleaned-up format suitable for ingestion by tools like Filebeat, which might forward it to Graylog for indexing and analysis.

This script is a data normalization and enrichment utility for audit logs. It:

  • Flattens nested structures.
  • Standardizes field names.
  • Adds useful metadata.
  • Cleans up irrelevant or sensitive info. It’s designed to make audit events searchable, readable, and index-friendly - especially in systems like Graylog or ELK (Elasticsearch, Logstash, Kibana).
filebeat.inputs:
- type: filestream
  id: atlassian-audit
  enabled: true
  encoding: utf-8
  paths:
    - "/opt/bitbucket/atlassian/bitbucket_home/log/audit/*.audit.log"
  ignore_older: 24h # don't even bother processing log files older than 24 hours
  ignore_inactive: since_last_start # read from files that have been updated since starting filebeat
  clean_inactive: 168h # for log files older than 7 days, clean from tracking registry
  fields_under_root: true
  fields:
    type: audit # add 'type' record indicating this is an audit record for Graylog Data Routing
    application_name: bitbucket
    deployment: production # or 'test', 'staging', etc.
  parsers:
    - ndjson: # parse each log line as JSON, and put the keys in an "audit" object
        target: "audit"
        add_error_key: true
        expand_keys: true

output.logstash:
 # Set via Sidecar variables, or edit file and embed hardcoded values
  hosts: ["${GRAYLOG_HOST}:${BEATS_PORT}"]
processors:
  - script:
      lang: javascript # Note! Filebeat uses a subset of JavaScript based on the Goja interpreter
      id: concatenate_fields_and_remove_i18n_keys_and_empty_arrays
      source: >
        (insert transformation script here - see below)
  - drop_fields:
     ignore_missing: true
     fields:
       - "agent"
       - "ecs"
       - "log"
       - "input"
       - "host.name"

setup: # don't load the ingest pipelines for Elasticsearch since we aren't using it
  ilm.enabled: false
  template.enabled: false

logging:
  level: warning # only log warnings and errors; change to 'info' for debugging
  to_files: true
  files:
    path: /var/log/filebeat
    name: filebeat
    keepfiles: 3
    permissions: 0640
  metrics.enabled: false # don't log Filebeat metrics
Enter fullscreen mode Exit fullscreen mode

Sample Transformation script in JavaScript

function process(event) {
    event.Put('event_received_time', new Date());

    function toCamelCase(str) {
        return str.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (match, chr) => chr.toUpperCase());
    }
    (event.Get('audit.affectedObjects') || []).forEach((o) => {
        if (o.type && o.name) {
            event.Put(`audit.affectedObject.${toCamelCase(o.type)}`, o.name);
        }
    });
    event.Delete('audit.affectedObjects');
    (event.Get('audit.extraAttributes') || []).forEach((a) => {
        if (a.name && a.value && a.name !== 'Load balancer/proxy IP address') {
            event.Put(`audit.extraAttribute.${toCamelCase(a.name)}`, a.value);
        }
    });
    event.Delete('audit.extraAttributes');
    (event.Get('audit.changedValues') || []).forEach((c) => {
        if (!c.key) {
            return;
        }
        const k = `audit.changedValue.${toCamelCase(c.key)}`;
        const d = c.from && c.to ? (`${c.from}${c.to}`) : (c.to ||
            (c.from ? `from ${c.from}` : ''));
        if (d) {
            event.Put(k, d);
        }
    });
    event.Delete('audit.changedValues');
    const ms = Number(event.Get('audit.timestamp.epochSecond') ||
        0) * 1000 + Math.floor(Number(event.Get('audit.timestamp.nano') || 0) /
        1e6);
    event.Put('@timestamp', new Date(ms));
    event.Delete('audit.timestamp');
    const author = event.Get('audit.author.name') || '';
    event.Put('user_name', author);
    event.Put('message',
        ((event.Get('audit.auditType.action') || '') + (author && author !== 'Anonymous' ? ` by ${author}` : '')));
    // Clean up i18n keys and other unwanted fields
    event.Delete('audit.auditType.actionI18nKey');
    event.Delete('audit.auditType.categoryI18nKey');
    event.Delete('audit.author.uri');
    return event;
}
// Run 'filebeat -c filebeat.yml test config' to make sure script code is functioning as expected
function test() {
    const sampleEvent = {
        audit: {
            auditType: {
                action: 'Pull request participants changed',
                actionI18nKey: 'bitbucket.service.pullrequest.audit.action.pullrequestparticipantsupdatedevent',
                area: 'END_USER_ACTIVITY',
                category: 'Pull requests',
                categoryI18nKey: 'bitbucket.service.audit.category.pullrequests',
                level: 'BASE',
            },
            affectedObjects: [{
                    id: '1234',
                    name: 'PR#123',
                    type: 'PULL_REQUEST'
                },
                {
                    id: '5678',
                    name: 'testrepo',
                    type: 'REPOSITORY'
                },
                {
                    id: '3445',
                    name: 'PROJ',
                    type: 'PROJECT'
                },
            ],
            changedValues: [{
                i18nKey: 'bitbucket.service.pullrequest.audit.changedvalue.pullrequestparticipantsupdated.participants',
                key: 'Participants',
                to: 'bitbucket.system-user',
            }, ],
            extraAttributes: [{
                    name: 'From ref',
                    nameI18nKey: 'bitbucket.service.pullrequest.audit.attribute.pullrequest.fromref',
                    value: 'test-branch',
                },
                {
                    name: 'To ref',
                    nameI18nKey: 'bitbucket.service.pullrequest.audit.attribute.pullrequest.toref',
                    value: 'main',
                },
                {
                    name: 'Load balancer/proxy IP address',
                    value: '10.1.2.3',
                },
            ],
            author: {
                id: '-1',
                name: 'System',
                type: 'system',
            },
            timestamp: {
                epochSecond: 1755786989,
                nano: 582000000
            },
        },
    };
    const event = process(new Event(sampleEvent));
    // Test that i18n keys are removed
    if (event.Get('audit.auditType.actionI18nKey') !== null) {
        throw 'expected auditType.actionI18nKey to be removed';
    }
    if (event.Get('audit.auditType.categoryI18nKey') !== null) {
        throw 'expected auditType.categoryI18nKey to be removed';
    }
    // Test that flattened fields are created
    if (event.Get('audit.affectedObject.pullRequest') !== 'PR#123') {
        throw "expected audit.affectedObject.pullRequest to be 'PR#123'";
    }
    if (event.Get('audit.affectedObject.repository') !== 'testrepo') {
        throw "expected audit.affectedObject.repository to be 'testrepo'";
    }
    if (event.Get('audit.affectedObject.project') !== 'PROJ') {
        throw "expected audit.affectedObject.project to be 'PROJ'";
    }
    if (event.Get('audit.affectedObject.project') !== 'PROJ') {
        throw "expected audit.affectedObject.project to be 'PROJ'";
    }
    if (event.Get('audit.changedValue.participants') !== 'bitbucket.system-user') {
        throw "expected audit.changedValue.participants to be 'bitbucket.system-user'";
    }
    if (event.Get('audit.extraAttribute.fromRef') !== 'test-branch') {
        throw "expected audit.extraAttribute.fromRef to be 'test-branch'";
    }
    if (event.Get('audit.extraAttribute.toRef') !== 'main') {
        throw "expected audit.extraAttribute.toRef to be 'main'";
    }
    // Test that Load balancer IP is excluded
    if (event.Get('audit.extraAttribute.loadBalancerProxyIpAddress') !== null) {
        throw 'expected Load balancer IP to be excluded';
    }
    // Test that original arrays are deleted (we have individual fields now)
    if (event.Get('audit.affectedObjects') !== null) {
        throw 'expected audit.affectedObjects to be deleted';
    }
    if (event.Get('audit.changedValues') !== null) {
        throw 'expected audit.changedValues to be deleted';
    }
    if (event.Get('audit.extraAttributes') !== null) {
        throw 'expected audit.extraAttributes to be deleted';
    }
}
Enter fullscreen mode Exit fullscreen mode

Ref: Graylog Go 2025 Event - https://graylog.org/graylog-go/
https://app.zuddl.com/graylog/l/event/0ed76ccc-e143-4dfe-a693-9195db341937/booth/3d4ec2d3-06d0-49c9-ab9b-d772154b7792?idx=0

Top comments (0)