Jump to content

Architecture Guidance: How to ingest GCP Firewall\VPC logs into Microsoft Sentinel

Featured Replies

Posted

Firstly, I would like to thank Benjamin Kovacevic and Yael Bergman for their help with this article.

 

 

 

While the existing Sentinel GCP Pub/Sub Audit Logs connector documented here provides a way to ingest GCP platform audit logs, ingesting GCP Firewall logs or VPS logs remains a needed capability.

 

 

 

In this blog post I will show a simple way to ingest Google Cloud GCP Firewall logs or VPS logs.

 

 

 

In order to do this ingestion, I have utilized the Log Ingestion API with PowerShell to accomplish this.

 

 

 

Note: The code provided in this post is just a sample provided AS-IS. Further code optimization and additions could be added as required.

 

 

 

Following is the summary of tasks that are required in order to accomplish this from a high level:

 

 

 

1- Create Microsoft Entra application

 

2- Create data collection endpoint

 

3- Create new table in Log Analytics workspace

 

4- Create a new service account in the GCP project with the needed assigned GCP IAM role.

 

5- Create a new GCP PUBSUB topic and a new GCP PULL typed PUBSUB subscription.

 

6- Construct a JWT header and acquire a JWT token

 

7- Pull the PUBSUB messages from the PUBSUB REST API

 

8- Send a message ack back and ingest the message content into Sentinel

 

 

 

 

 

Detailed Steps

 

 

 

1- Create Microsoft Entra application

 

Working with Log Ingestion API requires to register a new App in Entra ID and note down the TenantId, AppId, Secret.

 

Steps to create a new App registration is documented here for reference.

 

 

 

2- Create data collection endpoint

 

The DCE is required in order to receive the incoming data stream. Required steps to create a new DCE is documented here.

 

 

 

3- Create new table in Log Analytics workspace

 

Before you can send data to the workspace, you need to create the custom table where the data will be sent. Required steps to create a new table is documented here.

 

 

 

What is remaining up to this point is to take note of the DCR ImmutableId and also assign role Monitoring Metrics Publisher to DCR as described in details in same document referenced above.

 

 

 

Note that the new table schema has to match exactly every ingested property's. For instance I have used following column names when creating the new table schema

 

 

 

TimeGenerated

 

publishTime

 

messageId

 

insertId

 

dest_ip

 

dest_port

 

protocol

 

src_ip

 

src_port

 

disposition

 

project_id

 

region

 

vm_name

 

zone

 

direction

 

ip_protocol

 

priority

 

reference

 

project_id

 

subnetwork_name

 

subnetwork_id

 

vpc_name

 

logName

 

receiveTimestamp

 

Logtype

 

timestamp

 

 

 

4- Create a new service account in the GCP project with the needed assigned GCP IAM role.

 

 

 

5- Create a new GCP PUBSUB topic and a new GCP PULL typed PUBSUB subscription.

 

Here for both #4 and #5 we move to do some GCP side configurations. As a start we need to create a new service account which we are going to impersonate and this service account has to be assigned the correct IAM roles to enable it to pull and ack messages from the PUBSUB.

 

 

 

Steps to create a new service account and a new role in GCP in addition to creating a new PUBSUB topic and log sink are explained in details in this document.

 

 

 

Note: Here also note that it's recommended to create a filter to only have Firewall\VPC logs in this subscription while creating the sink.

 

 

 

6- Construct a JWT header and acquire a JWT token

 

Following this GCP authentication documentation here, we could construct a JWT header that we can use to acquire an access token.

 

 

 

7- Pull the PUBSUB messages from the PUBSUB REST API

 

To accomplish this step we could use the standard Method: projects.subscriptions.pull as documented here. Note that the ReceivedMessage in the response body contains an object called PubsubMessage where the data field is a Base64 encoded (PubsubMessage) string that will need to be decoded in the code before ingestion in Sentinel.

 

 

 

8- Send a message ack back and ingest the message content into Sentinel

 

Here an ack is recommended to be sent back in order to have that message removed from the PUBSUB subscription queue.

 

 

 

Now to the code work

 

 

 

  • Putting some needed parameters upfront

 

 

 

$appId = "xxxxxxxxxxxxxxxxxxxxxxxxxx"

$appSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"

$tenantId = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

 

$ingestionuri = "https://xxxxxxxxxxxxxxxxx.westeurope-1.ingest.monitor.azure.com"

$CertFile = "C:\xxxx\sentinel.p12"

$CertPassword = "notasecret"

$Project = "xxxxxxxx"

$ServiceAccountName = "sentinelserviceaccount"

$ServiceAccount = "sentinelserviceaccount@xxxxxxxxxxxxxx.iam.gserviceaccount.com"

$Scope = "https://www.googleapis.com/auth/pubsub"

$ExpirationSeconds = 3600

 

 

 

 

 

 

 

  • Function that constructs JWT header and acquire access token

 

 

 

 

 

 

function CreateJWT {

 

Write-Host "Attempting to obtain JWT access token"

# import certificate

$Certificate = [system.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertFile,$CertPassword,[system.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)

$RSACryptoServiceProvider = New-Object System.Security.Cryptography.RSACryptoServiceProvider

$RSACryptoServiceProvider.ImportParameters($Certificate.PrivateKey.ExportParameters($true))

 

# create JWT Header

$JwtHeader = '{"alg":"RS256","typ":"JWT"}'

$JwtHeaderBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($JwtHeader))

$JwtHeaderBase64UrlEncoded = $JwtHeaderBase64 -replace "/","_" -replace "\+","-" -replace "=", ""

 

# create JWT Claim Set

$Now = (Get-Date).ToUniversalTime()

$NowUnixTimestamp = [Math]::Floor([decimal](Get-Date -Date $Now -UFormat "%s"))

$ExpirationUnixTimestamp = $NowUnixTimestamp + $ExpirationSeconds

$JwtClaimSet = @"

{

"iss":"$ServiceAccount",

"scope":"$Scope",

"aud":"https://oauth2.googleapis.com/token",

"exp":$ExpirationUnixTimestamp,

"iat":$NowUnixTimestamp

}

"@

$JwtClaimSetBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($JwtClaimSet))

$JwtClaimSetBase64UrlEncoded = $JwtClaimSetBase64 -replace "/","_" -replace "\+","-" -replace "=", ""

 

# calculate Signature

$StringToSign = $JwtHeaderBase64UrlEncoded + "." + $JwtClaimSetBase64UrlEncoded

$SHA256 = [system.Security.Cryptography.SHA256]::Create()

$Hash = $SHA256.ComputeHash([Text.Encoding]::UTF8.GetBytes($StringToSign))

$SignatureBase64 = [Convert]::ToBase64String($RSACryptoServiceProvider.SignData([system.Text.Encoding]::UTF8.GetBytes($StringToSign),"SHA256"))

$SignatureBase64UrlEncoded = $SignatureBase64 -replace "/","_" -replace "\+","-" -replace "=", ""

 

# create JWT

$Jwt = $JwtHeaderBase64UrlEncoded + "." + $JwtClaimSetBase64UrlEncoded + "." + $SignatureBase64UrlEncoded

 

# send JWT request for oauth access token

$Body = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=$Jwt"

$AccessToken = Invoke-RestMethod -Method Post -Uri "https://oauth2.googleapis.com/token" -ContentType "application/x-www-form-urlencoded" -Body $Body | Select-Object -ExpandProperty access_token

 

Write-Host "Access token has been obtained successfully" #; "-"*50

Write-Host " "

return $AccessToken

}

 

 

 

 

 

 

  • Function that pulls messages from PUBSUB

 

 

 

 

 

 

 

function pull-messages {

Write-Host "Attempting to pull PUBSUB messages"

$Pulluri = "https://pubsub.googleapis.com/v1/projects/sentinel-403712/subscriptions/sentinel-sub:pull"

$PullBody = @{

maxMessages = "100"

}

$pulledmessages = Invoke-RestMethod -Method Post -Uri $Pulluri -Headers @{"Authorization"="Bearer $JWTToken"} -Body ($PullBody | ConvertTo-Json) -ContentType 'application/json'

 

Write-Host "PUBSUB Messages have been obtained successfully"

write-host " "

return $pulledmessages

}

 

 

 

 

 

 

  • Function that sends back an acknowledgement

 

 

 

 

 

 

function ackmsg {

 

 

param (

[string]$ackidss

)

$AckBody = @{

"ackIds" = $ackidss

}

$ackuri = "https://pubsub.googleapis.com/v1/projects/xxxxxxxxxx/subscriptions/xxxxxxxxxxx:acknowledge"

$autoack = Invoke-RestMethod -Method Post -Uri $ackuri -Headers @{"Authorization"="Bearer $JWTToken"} -Body ($AckBody | ConvertTo-Json) -ContentType 'application/json'

 

}

 

 

 

 

 

 

 

  • Function that do the actual ingestion into the new table

 

Note that more data processing is done in order to decode the base64 encoded data and also to do some string manipulation to have all properties ready in the right shape for the ingestion.

 

 

 

 

 

 

function ingestmsg{

 

param (

[PSCustomObject]$mmm

 

)

 

# $mmm

$mmm = $mmm -replace ";", ""

$mmm = $mmm -replace "}", ""

$msgobjj = $mmm | ConvertFrom-String -PropertyNames data, attributes,messageId,publishTime

 

 

$msgobjj.data = $msgobjj.data -replace "@{data=", ""

$msgobjj.attributes = $msgobjj.attributes -replace "attributes=", ""

$msgobjj.messageId = $msgobjj.messageId -replace "messageId=", ""

$msgobjj.publishTime = $msgobjj.publishTime -replace "publishTime=" , ""

 

$msgobjj.data = [system.Text.Encoding]::UTF8.GetString([system.Convert]::FromBase64String($msgobjj.data))

 

 

 

$TimeGenerated = Get-Date ([datetime]::UtcNow) -Format O

$data = $msgobjj.data | ConvertFrom-Json

$publishTime = $msgobjj.publishTime

$messageId = $msgobjj.messageId

$InsertId = $data.InsertId

$dest_ip = $data.jsonPayload.connection.dest_ip

$dest_port= $data.jsonPayload.connection.dest_port

$protocol = $data.jsonPayload.connection.protocol

$src_ip = $data.jsonPayload.connection.src_ip

$src_port = $data.jsonPayload.connection.src_port

$disposition = $data.jsonPayload.disposition

$project_id = $data.jsonPayload.instance.project_id

$region = $data.jsonPayload.instance.region

$vm_name = $data.jsonPayload.instance.vm_name

$zone = $data.jsonPayload.instance.zone

$direction = $data.jsonPayload.rule_details.direction

$ip_protocol = $data.jsonPayload.rule_details.ip_port_info.ip_protocol

$priority = $data.jsonPayload.rule_details.priority

$reference = $data.jsonPayload.rule_details.reference

$project_id = $data.jsonPayload.instance.project_id

$subnetwork_name = $data.resource.labels.subnetwork_name

$subnetwork_id = $data.resource.labels.subnetwork_id

$vpc_name = $data.jsonPayload.vpc.vpc_name

$logName = $data.logName

$timestamp = $data.timestamp

$type = $data.resource.type

$receiveTimestamp = $data.receiveTimestamp

 

 

$staticData = @"

[

{

 

"TimeGenerated": "$TimeGenerated",

"publishTime": "$publishTime",

"messageId": "$messageId",

"insertId": "$InsertId",

"dest_ip": "$dest_ip",

"dest_port": "$dest_port",

"protocol": "$protocol",

"src_ip": "$src_ip",

"src_port": "$src_port",

"disposition":"$disposition",

"project_id":"$project_id",

"region":"$region",

"vm_name":"$vm_name",

"zone":"$zone",

"direction":"$direction",

"ip_protocol":"$ip_protocol",

"priority":"$priority",

"reference":"$reference",

"project_id":"$project_id",

"subnetwork_name":"$subnetwork_name",

"subnetwork_id":"$subnetwork_id",

"vpc_name":"$vpc_name",

"logName":"$logName",

"receiveTimestamp":"$dtimestamp",

"Logtype":"$resource.type",

"timestamp":"$receiveTimestamp"

}

 

]

"@;

Add-Type -AssemblyName System.Web

 

$appId = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

$appSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

$tenantId = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

 

 

$scope= [system.Web.HttpUtility]::UrlEncode("https://monitor.azure.com//.default")

$body = "client_id=$appId&scope=$scope&client_secret=$appSecret&grant_type=client_credentials";

$headers = @{"Content-Type"="application/x-www-form-urlencoded"};

$uri = "Sign in to your account"

$bearerToken = (Invoke-RestMethod -Uri $uri -Method "Post" -Body $body -Headers $headers).access_token

 

 

$dceEndpoint = "https://xxxxxxxxxxxxxxxxxx.westeurope-1.ingest.monitor.azure.com" #the endpoint property of the Data Collection Endpoint object

$dcrImmutableId = "dcr-xxxxxxxxxxxxxxxxxxxxxxxxxxxx" #the immutableId property of the DCR object

$streamName = "Custom-GcpFWLogs_CL" #name of the stream in the DCR that represents the destination table

 

 

$body = $staticData;

$headers = @{"Authorization"="Bearer $bearerToken";"Content-Type"="application/json"};

$urii = "$dceEndpoint/dataCollectionRules/$dcrImmutableId/streams/$($streamName)?api-version=2021-11-01-preview"

 

$uploadResponse = Invoke-RestMethod -Uri $urii -Method "Post" -Body $body -Headers $headers

 

}

 

 

 

 

 

 

 

  • The actual code start running from here

 

 

 

 

 

 

 

$JWTToken = CreateJWT

$messages = pull-messages

$msgcount = ($messages.receivedMessages).count

Write-Host ""

write-host "Message count is:" $msgcount; ""

$msg = $messages.receivedMessages.message

if($msgcount -eq 1 ){

$ack = $messages.receivedMessages.ackid

write-host "Attempting to ack message by calling ack function"

ackmsg -ackidss $ack

Write-Host "message has been acked successfully"

$msg = $messages.receivedMessages.message

write-host "Attempting to ingest message by calling ingest function"

ingestmsg $msg

 

Write-Host "message has been ingested successfully"

 

}

 

 

<# Action when all if and elseif conditions are false #>

if($msgcount -eq 0 )

{

Write-Host "no new messages"

}

 

 

 

 

if($msgcount -gt 1 )

{

 

for ($i=1; $i -lt $msgcount; $i++)

{

$acks = $messages.receivedMessages.ackid

$acked = $acks[$i]

 

write-host "Attempting to ack message" $i "by calling ack function"

 

ackmsg -ackidss $acked

 

Write-Host "message" $i "has been acked successfully"

 

$msgs = $messages.receivedMessages.message

$msgg = $msgs[$i]

 

write-host "Attempting to ingest message" $i "by calling ingest function"

ingestmsg $msgg

 

Write-Host "message" $i "has been ingested successfully"

}

 

 

}

 

 

 

 

 

 

How data appears in Sentinel:

 

 

 

[ATTACH=full]55507[/ATTACH]

 

 

 

Final notes:

 

  • This is a sample of code on how it could be done. This could be created in a form of Azure FunctionApp with a scheduled recurrence for instance.
  • Some error handling could also be added for better monitoring of this ingestion pipeline.

 

Continue reading...

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...