Continuing from the previous post where I exported data from Log Analytics to an Event Hub I now want to capture events from an Event Hub and do an HTTP trigger to an external REST API. I can do this via Logic Apps or Azure Functions (and others too I suppose but these are the two I tried).
Update (19 Oct 2021): The Functions way doesn’t really work so if you are reading this post for that feel free to skip this one and read my later post instead. Of course this one’s a good read to know what stupid mistakes I made. 🤦♂️
Logic Apps
I started with this as I figure this would be easier. I am going to skip the basic steps here like how to create a Logic App etc.
First get stuff from the Event Hub. For this we need to create a connection to it. While doing this the first time I thought I could get away with an access policy that has only the Listen
claim but that didn’t help. Looks like you need an access policy with all three rights, similar to the default one. So that’s something to bear in mind.
When creating the trigger initially I left things at the default and just ran it to see what the output looks like. It was like this:
That seems to be some base64 encoded data. Then I realized I can change the content type in the trigger to application/json
.
Did that and I get better results.
The output is now JSON. It contains a single “record
” property that is an array. Each element of the array is the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{ "TenantId": "xxxxx", "TimeGenerated": "xxxxx", "SourceSystem": "xxxxx", "Account": "xxxxx", "AccountType": "xxxxx", "Computer": "xxxxx", "EventSourceName": "xxxxx", "Channel": "xxxxx", "Task": "xxxxx", "Level": "xxxxx", "EventID": "xxxxx", "Activity": "xxxxx", "SourceComputerId": "xxxxx", "EventOriginId": "xxxxx", "MG": "xxxxx", "TimeCollected": "xxxxx", "ManagementGroupName": "xxxxx", "PrivilegeList": "xxxxx", "SubjectAccount": "xxxxx", "SubjectDomainName": "xxxxx", "SubjectLogonId": "xxxxx", "SubjectUserName": "xxxxx", "SubjectUserSid": "xxxxx", "Type": "xxxxx", "_Internal_WorkspaceResourceId": "xxxxx", "_ResourceId": "/subscriptions/xxxxxx/resourceGroups/yyyyyy/providers/Microsoft.Compute/virtualMachines/aaaaaa" } |
I copied everything in that Content box in the output. Then I went back to the Logic App and added a Parse JSON
action. I pasted the text I copied above into the sample payload section and so Logic Apps helpfully generated a schema.
Here’s the schema just in case anyone’s following along:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
{ "properties": { "records": { "items": { "properties": { "Account": { "type": "string" }, "AccountType": { "type": "string" }, "Activity": { "type": "string" }, "AuthenticationPackageName": { "type": "string" }, "Channel": { "type": "string" }, "Computer": { "type": "string" }, "EventData": { "type": "string" }, "EventID": { "type": "string" }, "EventOriginId": { "type": "string" }, "EventSourceName": { "type": "string" }, "ImpersonationLevel": { "type": "string" }, "IpAddress": { "type": "string" }, "IpPort": { "type": "string" }, "KeyLength": { "type": "string" }, "Level": { "type": "string" }, "LmPackageName": { "type": "string" }, "LogonGuid": { "type": "string" }, "LogonProcessName": { "type": "string" }, "LogonType": { "type": "string" }, "LogonTypeName": { "type": "string" }, "MG": { "type": "string" }, "ManagementGroupName": { "type": "string" }, "PrivilegeList": { "type": "string" }, "Process": { "type": "string" }, "ProcessId": { "type": "string" }, "ProcessName": { "type": "string" }, "SourceComputerId": { "type": "string" }, "SourceSystem": { "type": "string" }, "SubjectAccount": { "type": "string" }, "SubjectDomainName": { "type": "string" }, "SubjectLogonId": { "type": "string" }, "SubjectUserName": { "type": "string" }, "SubjectUserSid": { "type": "string" }, "TargetAccount": { "type": "string" }, "TargetDomainName": { "type": "string" }, "TargetLogonId": { "type": "string" }, "TargetUserName": { "type": "string" }, "TargetUserSid": { "type": "string" }, "Task": { "type": "string" }, "TenantId": { "type": "string" }, "TimeCollected": { "type": "string" }, "TimeGenerated": { "type": "string" }, "TransmittedServices": { "type": "string" }, "Type": { "type": "string" }, "WorkstationName": { "type": "string" }, "_Internal_WorkspaceResourceId": { "type": "string" }, "_ResourceId": { "type": "string" } }, "required": [ "TenantId", "TimeGenerated", "SourceSystem", "Computer", "EventSourceName", "Channel", "Task", "Level", "EventID", "Activity", "SourceComputerId", "EventOriginId", "MG", "TimeCollected", "ManagementGroupName", "Type", "_Internal_WorkspaceResourceId", "_ResourceId" ], "type": "object" }, "type": "array" } }, "type": "object" } |
Next I added a For each
action. I know the records
property is an array so I added that as the input to the For each
(that is the only option anyways).
Now I want to parse the individual entry. So I added an action for that too. For the schema I copy pasted an individual entry from the earlier output I captured from the Content box above and pasted that in. This gave me a schema:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
{ "type": "object", "properties": { "TenantId": { "type": "string" }, "TimeGenerated": { "type": "string" }, "SourceSystem": { "type": "string" }, "Account": { "type": "string" }, "AccountType": { "type": "string" }, "Computer": { "type": "string" }, "EventSourceName": { "type": "string" }, "Channel": { "type": "string" }, "Task": { "type": "string" }, "Level": { "type": "string" }, "EventID": { "type": "string" }, "Activity": { "type": "string" }, "SourceComputerId": { "type": "string" }, "EventOriginId": { "type": "string" }, "MG": { "type": "string" }, "TimeCollected": { "type": "string" }, "ManagementGroupName": { "type": "string" }, "AuthenticationPackageName": { "type": "string" }, "ImpersonationLevel": { "type": "string" }, "IpAddress": { "type": "string" }, "IpPort": { "type": "string" }, "KeyLength": { "type": "string" }, "LmPackageName": { "type": "string" }, "LogonGuid": { "type": "string" }, "LogonProcessName": { "type": "string" }, "LogonType": { "type": "string" }, "LogonTypeName": { "type": "string" }, "Process": { "type": "string" }, "ProcessId": { "type": "string" }, "ProcessName": { "type": "string" }, "SubjectAccount": { "type": "string" }, "SubjectDomainName": { "type": "string" }, "SubjectLogonId": { "type": "string" }, "SubjectUserName": { "type": "string" }, "SubjectUserSid": { "type": "string" }, "TargetAccount": { "type": "string" }, "TargetDomainName": { "type": "string" }, "TargetLogonId": { "type": "string" }, "TargetUserName": { "type": "string" }, "TargetUserSid": { "type": "string" }, "TransmittedServices": { "type": "string" }, "Type": { "type": "string" }, "WorkstationName": { "type": "string" }, "_Internal_WorkspaceResourceId": { "type": "string" }, "_ResourceId": { "type": "string" } } } |
Right ho! Let’s run that to see how things look.
Perfect! Now I can bung this off in an HTTP Request:
And that’s it!
(In reality I added an additional step to get the API key from a Key Vault and also some filtering of the JSON to skip records I am not interested in… but that doesn’t matter here).
Azure Function
Create a new function app. PowerShell’s all I know so that’s what I am going to use:
Go with the serverless plan.
Then create a new function. Select the Event Hub trigger template. Add a connection to the Event Hub (a policy using just the Listen
claim is sufficient). Put in the Event Hub name.
You now end up with a function like this but it doesn’t work:
1 2 3 4 5 |
param($eventHubMessages, $TriggerMetadata) Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages" $eventHubMessages | ForEach-Object { Write-Host "Processed message: $_" } |
The output is like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
2021-10-16T10:40:20.364 [Information] Executed 'Functions.EventHubTrigger1' (Succeeded, Id=eeea7a72-2b03-4c45-9f71-48e312e83659, Duration=146ms) 2021-10-16T10:40:20.434 [Information] Executing 'Functions.EventHubTrigger1' (Reason='(null)', Id=2c47176a-e485-422e-81ae-9597962695a2) 2021-10-16T10:40:20.434 [Information] Trigger Details: PartionId: 2, Offset: 73025121728-73025121728, EnqueueTimeUtc: 2021-10-16T10:40:20.2040000Z-2021-10-16T10:40:20.2040000Z, SequenceNumber: 6329-6329, Count: 1 2021-10-16T10:40:20.453 [Information] INFORMATION: PowerShell eventhub trigger function called for message array: System.Collections.Hashtable 2021-10-16T10:40:20.454 [Information] INFORMATION: Processed message: System.Collections.Hashtable 2021-10-16T10:40:20.455 [Information] Executed 'Functions.EventHubTrigger1' (Succeeded, Id=2c47176a-e485-422e-81ae-9597962695a2, Duration=27ms) 2021-10-16T10:40:20.792 [Information] Executing 'Functions.EventHubTrigger1' (Reason='(null)', Id=9e84634b-797b-40ae-9d88-a198d5a17835) 2021-10-16T10:40:20.792 [Information] Trigger Details: PartionId: 2, Offset: 73025445776-73025445776, EnqueueTimeUtc: 2021-10-16T10:40:20.7540000Z-2021-10-16T10:40:20.7540000Z, SequenceNumber: 6330-6330, Count: 1 2021-10-16T10:40:20.825 [Information] INFORMATION: PowerShell eventhub trigger function called for message array: System.Collections.Hashtable 2021-10-16T10:40:20.825 [Information] INFORMATION: Processed message: System.Collections.Hashtable 2021-10-16T10:40:20.826 [Information] Executed 'Functions.EventHubTrigger1' (Succeeded, Id=9e84634b-797b-40ae-9d88-a198d5a17835, Duration=42ms) 2021-10-16T10:40:21.199 [Information] Executing 'Functions.EventHubTrigger1' (Reason='(null)', Id=d98c5525-5e3b-45f6-bb57-b512ce8dc8fa) 2021-10-16T10:40:21.199 [Information] Trigger Details: PartionId: 2, Offset: 73025768160-73025768160, EnqueueTimeUtc: 2021-10-16T10:40:21.1640000Z-2021-10-16T10:40:21.1640000Z, SequenceNumber: 6331-6331, Count: 1 |
Having no idea what’s wrong here, but knowing the objects seem to be hash tables I tried the following:
1 2 3 4 5 6 7 |
param($eventHubMessages, $TriggerMetadata) Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages" # $eventHubMessages | ForEach-Object { Write-Host "Processed message: $_" } $eventHubMessages | ForEach-Object { Write-Host "Processed message: $($_ | Get-Member) " } |
That gave the following error:
1 |
2021-10-16T10:42:58.840 [Error] Executed 'Functions.EventHubTrigger1' (Failed, Id=4379689c-e1fb-44ab-8609-586e7389384c, Duration=6ms)Binding parameters to complex objects (such as 'Object') uses Json.NET serialization.1. Bind the parameter type as 'string' instead of 'Object' to get the raw values and avoid JSON deserialization, or2. Change the queue payload to be valid json. The JSON parser failed: Unexpected character encountered while parsing value: T. Path '', line 0, position 0. |
Trying to access a single property didn’t help either:
1 2 |
# Since I know the JSON has the Computer property (based on what I did with Logic Apps above) $eventHubMessages | ForEach-Object { Write-Host "Processed message: $($_.Computer) " } |
Hmm.
Googling on this latest error got me to this GitHub issue though. Looks like I should specify the binding as string.
That’s under the “Integration” section, so I did just that:
Select “String”:
I reverted the code back to the default:
1 2 3 4 5 |
param($eventHubMessages, $TriggerMetadata) Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages" $eventHubMessages | ForEach-Object { Write-Host "Processed message: $_" } |
Any better now? Yes:
1 |
2021-10-16T12:38:14.344 [Information] INFORMATION: Processed message: {"records": [{ .... |
I truncated the output as there’s a lot. As before though I was looking at a records
property that was an array. Thing is, since I said this was a string I figured I’ll have to convert it to JSON first and then try to manipulate. So I did the following:
1 2 3 4 5 6 7 8 9 10 11 12 |
param($eventHubMessages, $TriggerMetadata) # Commenting this out to reduce the noise # Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages" $eventHubMessages | ConvertFrom-JSON | ForEach-Object { Write-Host "==== Message Array ====" foreach ($record in $_.records) { Write-Host "**** JSON ****" ConvertTo-JSON $record -Depth 10 } } |
That works!
1 2 3 4 |
2021-10-16T13:05:26.890 [Information] INFORMATION: **** JSON **** 2021-10-16T13:05:26.891 [Information] OUTPUT: {"TenantId": "xxxxx",,"TimeGenerated": "xxxxx",,"SourceSystem": "xxxxx",,"Account": "xxxxx",,"AccountType": "xxxxx",,"Computer": "xxxxx",,"EventSourceName": "xxxxx",,"Channel": "xxxxx",,"Task": "xxxxx",,"Level": "xxxxx",,"EventID": "xxxxx",,"Activity": "xxxxx",,"SourceComputerId": "xxxxx",,"EventOriginId": "xxxxx",,"MG": "xxxxx",,"TimeCollected": "xxxxx",,"ManagementGroupName": "xxxxx",,"AuthenticationPackageName": "xxxxx",,"ImpersonationLevel": "xxxxx",,"IpAddress": "xxxxx",,"IpPort": "xxxxx",,"KeyLength": "xxxxx",,"LmPackageName": "xxxxx",,"LogonGuid": "xxxxx",,"LogonProcessName": "xxxxx",,"LogonType": "xxxxx",,"LogonTypeName": "xxxxx",,"Process": "xxxxx",,"ProcessId": "xxxxx",,"ProcessName": "xxxxx",,"SubjectAccount": "xxxxx",,"SubjectDomainName": "xxxxx",,"SubjectLogonId": "xxxxx",,"SubjectUserName": "xxxxx",,"SubjectUserSid": "xxxxx",,"TargetAccount": "xxxxx",,"TargetDomainName": "xxxxx",,"TargetLogonId": "xxxxx",,"TargetUserName": "xxxxx",,"TargetUserSid": "xxxxx",,"TransmittedServices": "xxxxx",,"Type": "xxxxx",,"WorkstationName": "xxxxx",,"_Internal_WorkspaceResourceId": "xxxxx",,"_ResourceId": "xxxxx",} 2021-10-16T13:05:26.891 [Information] INFORMATION: **** JSON **** 2021-10-16T13:05:26.891 [Information] OUTPUT: {"TenantId": "xxxxx",,"TimeGenerated": "xxxxx",,"SourceSystem": "xxxxx",,"Account": "xxxxx",,"AccountType": "xxxxx",,"Computer": "xxxxx",,"EventSourceName": "xxxxx",,"Channel": "xxxxx",,"Task": "xxxxx",,"Level": "xxxxx",,"EventID": "xxxxx",,"Activity": "xxxxx",,"SourceComputerId": "xxxxx",,"EventOriginId": "xxxxx",,"MG": "xxxxx",,"TimeCollected": "xxxxx",,"ManagementGroupName": "xxxxx",,"LogonType": "xxxxx",,"LogonTypeName": "xxxxx",,"TargetAccount": "xxxxx",,"TargetDomainName": "xxxxx",,"TargetLogonId": "xxxxx",,"TargetUserName": "xxxxx",,"TargetUserSid": "xxxxx",,"Type": "xxxxx",,"_Internal_WorkspaceResourceId": "xxxxx",,"_ResourceId": "xxxxx",} |
I added those extra text markers in there so I can easily identify the individual JSON pieces. In the final code I can remove those:
1 2 3 4 5 6 7 8 9 10 |
param($eventHubMessages, $TriggerMetadata) # Commenting this out to reduce the noise # Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages" $eventHubMessages | ConvertFrom-JSON | ForEach-Object { foreach ($record in $_.records) { ConvertTo-JSON $record -Depth 10 } } |
All I need to do now is post this to a REST API. That’s easy stuff.
Update (19 Oct 2021): Turns out I might have mistaken coz when I got to the supposedly easy stuff of posting this via a REST API I realized that the output isn’t formatted well. Here’s an example of what it looked like on the receiving side:
1 2 3 4 5 6 7 8 9 10 |
[ "{\r", " \"AccountType\": \"User\",\r", " \"Level\": \"8\",\r", " \"MG\": \"00000000-0000-0000-0000-000000000001\",\r", " \"ImpersonationLevel\": \"%%1833\",\r", " \"LogonGuid\": \"00000000-0000-0000-0000-000000000000\",\r", " \"ProcessId\": \"0x274\",\r", " \"SubjectLogonId\": \"0x3e7\",\r", " \"TargetLinkedLogonId\": \"0x0\",\r", |
When I output it to the logs it looks fine, but when POSTing it it’s messed up. I think something about the conversion from JSON to hash table and back is messing things up. I tried alternatives like ConvertFrom-JSON -AsHashTable
(because the default is to convert it to a PSCustomObject
) but that didn’t help either.
Another thing I noticed is that each $eventHubMessages
object itself could contain many records
JSON arrays. So I tried the following but no luck:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
foreach ($eventHubMessage in $eventHubMessages) { $recordArray = ConvertFrom-JSON -AsHashTable -Depth 10 $eventHubMessage foreach ($record in $recordArray.records) { Write-Host $record.Computer " " $record.EventID if ($interestedComputers -contains $record.Computer -and $interestedEvents -contains $record.EventID) { Write-Host "Found interested EventID ($record.EventID) from computer ($record.Computer)" $request = Invoke-WebRequest -Uri $Url -Method "POST" -Headers $headers -ContentType $contentType -Body (ConvertTo-JSON -Depth 10 $record) if ($request.StatusCode -eq 200) { Write-Host "Successfully pushed to Qomplx" } else { Write-Host "Error " $request.StatusCode } } else { # Write-Host "Skipping record" } } } |
I am converting the incoming object from a JSON array to an array of hash tables and then iterating through it. It works fine on paper – I can get the Computer name and EventID etc. but when I push it out it’s f**ed up. :-/
Anyways, I don’t have too much time to spend on this (already wasted a day and weekend) so I think I’ll stick to Logic Apps for now.
Update (20 Oct 2021): I wonder if it’s related to this PowerShell Core issue on GitHub. There’s a SO post too I found from the GitHub issue.
Update (25 Oct 2021): I now have this working. It was just a case of me not understanding how Functions work. I first talk about my folly in this blog post and later in this blog post.