Template Strings and Transforms
  • 22 Apr 2024
  • 5 Minutes to read
  • Contributors
  • Dark

Template Strings and Transforms

  • Dark

Article summary

Many areas of LimaCharlie support template strings and transforms.

A template string allows you to customize the value of a configuration based on the context. For example to adjust the Detection Name in a D&R rule to include a value from the detection itself. Transforms can also be used to select, modify, or remove fields upon data ingestion from an Adapter.

A transform allows you to change the shape of JSON data in flight to suit better your usage. This can mean moving, renaming, removing and adding fields in JSON. For example, it can allow you to create an Output that works with DNS_REQUEST events, but outputs only specific fields from the event.

Template Strings

Template strings in LimaCharlie use the format defined by "text templates" found here. A useful guide provided by Hashicorp is also available here.

The most basic example for a D&R rule customizing the detection name looks like this:

- action: report
  name: Evil executable on {{ .routing.hostname }}

Template strings also support some LimaCharlie-specific functions:

  • token: applies an MD5 hashing function on the value provided.
  • anon: applies an MD5 hashing function on a secret seed value, plus the value provided.
  • json: marshals the input into a JSON string representation.

The token and anon functions can be used to partially anonymize data anywhere a template string is supported, for example:

- action: report
  name: 'User {{token .event.USER_NAME }} accessed a website against policy.'

Template Strings and Adapter Transforms

Template strings can also be used with in conjunction the client_options.mapping.transform option in Adapter configuration. These allow you to modify data prior to ingestion, having control over what fields get ingested and resulting field names.

The following options are available in Adapter configurations:

  • + to add a field
  • - to remove a field

Both support template strings, meaning you can add/remove values from the JSON data to replace/supplement other fields.

For example, if we had the following data:

{ "event": 
  "webster" : {
     "a" : 1,
     "b" : 2,
     "d" : 3

And we wanted to rename the d value to c on ingestion, remove the d value, and add a field called hostname, we could use the following configuration:

         +c : '{{ .webster.d }}',
         -d: nil,
         +hostname : '{{ "my-computer" }}',

The resulting event to be ingested would be:

{ "event": 
  "webster" : {
     "a" : 1,
     "b" : 2,
     "c" : 3
    "hostname" : "my-computer"


With Transforms, you specify a JSON object that describes the transformation.

This object is in the shape of the final JSON you would like to transform to.

Key names are the literal key names in the output. Values support one of 3 types:

  1. Template Strings, as described above. In this case, the template string will be generated and placed at the same place as the key in the transform object.
  2. A gjson selector. The selector syntaxt is defined here. It makes it possible to select subsets of input object and map it within the resulting object as defined by the transform.
  3. Other JSON objects which will be present in the output.

Let's look at an example, let's say this is the Input to our transform:

    "event": {
        "EVENT": {
            "EventData": {
                "AuthenticationPackageName": "NTLM",
                "FailureReason":             "%%2313",
                "IpAddress":                 "",
                "IpPort":                    "0",
                "KeyLength":                 "0",
                "LmPackageName":             "-",
                "LogonProcessName":          "NtLmSsp",
                "LogonType":                 "3",
                "ProcessId":                 "0x0",
                "ProcessName":               "-",
                "Status":                    "0xc000006d",
                "SubStatus":                 "0xc0000064",
                "SubjectDomainName":         "-",
                "SubjectLogonId":            "0x0",
                "SubjectUserName":           "-",
                "SubjectUserSid":            "S-1-0-0",
                "TargetDomainName":          "",
                "TargetUserName":            "ADMINISTRADOR",
                "TargetUserSid":             "S-1-0-0",
                "TransmittedServices":       "-",
                "WorkstationName":           "-",
            "System": {
                "Channel":  "Security",
                "Computer": "demo-win-2016",
                "Correlation": {
                    "ActivityID": "{F207C050-075F-0001-AFE1-ED1F3897D801}",
                "EventID":       "4625",
                "EventRecordID": "2832700",
                "Execution": {
                    "ProcessID": "572",
                    "ThreadID":  "2352",
                "Keywords": "0x8010000000000000",
                "Level":    "0",
                "Opcode":   "0",
                "Provider": {
                    "Guid": "{54849625-5478-4994-A5BA-3E3B0328C30D}",
                    "Name": "Microsoft-Windows-Security-Auditing",
                "Security": "",
                "Task":     "12544",
                "TimeCreated": {
                    "SystemTime": "2022-07-15T22:48:24.996361600Z",
                "Version": "0",
    "routing": {
        "arch":       2,
        "did":        "b97e9d00-ca17-4afe-a9cf-27c3468d5901",
        "event_id":   "f24679e5-5484-4ca1-bee2-bfa09a5ba3db",
        "event_time": 1657925305984,
        "event_type": "WEL",
        "ext_ip":     "",
        "hostname":   "demo-win-2016.c.lc-demo-infra.internal",
        "iid":        "7d23bee6-aaaa-aaaa-aaaa-c8e8cca132a1",
        "int_ip":     "",
        "moduleid":   2,
        "oid":        "8cbe27f4-aaaa-aaaa-aaaa-138cd51389cd",
        "plat":       268435456,
        "sid":        "bb4b30af-ff11-4ff4-836f-f014ada33345",
        "tags": [
        "this": "c5e16360c71baf3492f2dcd962d1eeb9",
    "ts": "2022-07-15 22:48:25",

And this is our Transform definition:

    "message": "Interesting event from {{ .routing.hostname }}",  // a format string
    "from":    "{{ \"limacharlie\" }}",                           // a format string with only a literal value
    "dat": {                                                      // define a sub-object in the output
        "raw": "event.EVENT.EventData"                            // a "raw" key where we map a specific object from the input
    "anon_ip": "{{anon .routing.int_ip }}",                       // an anonymized version of the internal IP
    "ts":   "routing.event_time",                                 // map a specific simple value
    "nope": "does.not.exist"                                      // map a value that is not present

Then the resulting Output would be:

    "dat": {
        "raw": {
            "AuthenticationPackageName": "NTLM",
            "FailureReason": "%%2313",
            "IpAddress": "",
            "IpPort": "0",
            "KeyLength": "0",
            "LmPackageName": "-",
            "LogonProcessName": "NtLmSsp",
            "LogonType": "3",
            "ProcessId": "0x0",
            "ProcessName": "-",
            "Status": "0xc000006d",
            "SubStatus": "0xc0000064",
            "SubjectDomainName": "-",
            "SubjectLogonId": "0x0",
            "SubjectUserName": "-",
            "SubjectUserSid": "S-1-0-0",
            "TargetDomainName": "",
            "TargetUserName": "ADMINISTRADOR",
            "TargetUserSid": "S-1-0-0",
            "TransmittedServices": "-",
            "WorkstationName": "-"
    "from": "limacharlie",
    "message": "Interesting event from demo-win-2016.c.lc-demo-infra.internal",
    "nope": null,
    "ts": 1657925305984,
    "anon_ip": "e80b5017098950fc58aad83c8c14978e"

Custom Modifiers

Beyond the built-in modifiers for gjson (as seen in their playground, LimaCharlie also implements several new modifiers:

  • parsejson: this modifier takes no arguments, it takes in as input a string that represents a JSON object and outputs the decoded JSON object.
  • extract: this modifier takes a single argument, re which is a regular expression that uses "named capture groups" (as defined in the re2 documentation). The group names become the keys of the output JSON object with the matching values.
  • parsetime: this modifier takes two arguments, from and to. It will convert an input string from a given time format (as defined in the Go time library format here) and outputs the resulting time in the to format. Beyond the time constants from the previous link, LimaCharlie also supports a from format of:
    • epoch_s: a second based epoch timestamp
    • epoch_ms: a millisecond based epoch timestamp

For example:
The transform:

  "new_ts": "ts|@parsetime:{\"from\":\"2006-01-02 15:04:05\", \"to\":\"Mon, 02 Jan 2006 15:04:05 MST\"}",
  "user": "origin|@extract:{\"re\":\".*@(?P<domain>.+)\"}"
  "ctx": "event.EVENT.exec_context|@parsejson"

applied to:

  "ts": "2023-05-10 22:35:48",
  "origin": "someuser@gmail.com",
  "event": {
    "EVENT": {
      "exec_context": "{\"some\": \"embeded value\"}"

would result in:

  "new_ts": "Wed, 10 May 2023 22:35:48 UTC",
  "user": {
    "domain": "gmail.com\""
  "ctx": {
    "some": "embeded value"

Was this article helpful?