Monitoring Windows Virtual Desktop environments (Fall 2019 release) with Azure Sentinel

This post has been republished via RSS; it originally appeared at: Azure Sentinel articles.

With thanks to  and  for their contributions to this blog post.

 

NOTE: This blog post covers monitoring resources using the Windows Virtual Desktop Fall 2019 release without Azure Resource Manager objects. If you are using the Windows Virtual Desktop Spring 2020 release with Azure Resource Manager objects (in Public Preview at the time of writing) then click here for details about how to connect this to your Sentinel workspace as the process and logs differ. Queries found in this blog will not work if you are using the WVD Spring 2020 deployment as tables and logs have changed.

 

Due to the COVID-19 health crisis, there has been an exponential increase in employees working from home and this has led to new challenges in the security monitoring space for SOC teams. We covered in two previous Tech Community articles how to monitor popular collaboration software – Teams and Zoom - using Azure Sentinel.

 

As part of this shift to remote work, some organizations have had to make rapid and sweeping changes to their endpoints. Windows Virtual Desktop (WVD) has enabled our customers to quickly provision Windows 10 virtual desktops to enable people who have traditionally not been remote workers to access a virtualized work desktop from home, and thus has enabled businesses to keep functioning. However, these new endpoints also need to be monitored to maintain an organization’s security posture and so in this blog, we will explore how you can use Azure Sentinel to monitor your WVD environment.

 

Overview of telemetry available in WVD

You can collect several types of telemetry signals from a WVD environment that can be ingested into Azure Sentinel for security monitoring:

 

  • Windows event logs.
  • Microsoft Defender Advanced Threat Protection (MDATP) alerts.
  • Logs from the WVD PaaS service itself (aka. WVD diagnostics).

 

Below is a summary of how WVD logs are ingested into Log Analytics.

WVD.PNG

WVD diagnostic logs being ingested to Sentinel via Log Analytics. Diagram by .

 

Windows event logs

Windows event logs from the WVD environment are ingested into Azure Sentinel in the same manner as Windows event logs from other Windows machines outside of the WVD environment, so we won’t be covering this in detail in the blog post. In brief, you will need to install the Log Analytics agent (previously known as the OMS agent or the MMA agent) onto your Windows machine and configure the Windows event logs to be sent to the Log Analytics workspace. Click here for further information about how to install the Log Analytics agent; and for more information about how to configure Windows event logs to be forwarded to a Log Analytics workspace, click here.

 

MDATP alerts

Like Windows event logs, to configure MDATP for WVD you would follow the same onboarding procedure as you would with any other Windows endpoint. There is a detailed walkthrough on how to onboard endpoints to MDATP here. For further information about how to send MDATP alerts to Azure Sentinel using the product’s pre-wired connectors, click here.

 

WVD diagnostics

WVD diagnostics is a feature of the WVD PaaS service that logs information whenever someone assigned Windows Virtual Desktop role uses the service. Each log contains information about which Windows Virtual Desktop role was involved in the activity, any error messages that appear during the session, tenant information, and user information. The diagnostics feature creates activity logs for both user and administrative actions. For more information about WVD diagnostic logs for the Fall 2019 release of WVD, click here.

 

 

Ingesting WVD diagnostic logs into Azure Sentinel

 

Before you start

We need to configure WVD to send diagnostics to a Log Analytics workspace. If you have multiple Log Analytics workspaces in your environment, you will need decide which one you are going to send WVD diagnostic logs to.

 

NOTE: Different WVD tenants can be configured to send their diagnostics to different workspaces, so if you have multiple WVD tenants and Log Analytics workspaces within your environment – e.g. workspaces in different Azure regions for data sovereignty – this posture can be maintained.

 

Obtain your chosen Log Analytics workspace ID and the primary key; you will need this later in our setup. If you have never obtained your Log Analytics workspace ID and primary key before, details about how to get this workspace information can be found here.

 

Pushing WVD diagnostics to the Log Analytics workspace

If you’ve already created your WVD tenant, run the following PowerShell command to link the WVD tenant to your chosen Log Analytics workspace:

 

Set-RdsTenant -Name <TenantName> -AzureSubscriptionId <SubscriptionID> -LogAnalyticsWorkspaceId <String> -LogAnalyticsPrimaryKey <String>

 

If you’re creating a new WVD tenant, you can link it to your chosen Log Analytics workspace by running the following cmdlet to sign in to Windows Virtual Desktop with your TenantCreator user account:

Add-RdsAccount -DeploymentUrl https://rdbroker.wvd.microsoft.com

 

NOTE: As per the note above, you will need to complete one of the following operations for every WVD tenant individually to link it to a Log Analytics workspace.

 

Using WVD diagnostics in Azure Sentinel

 

WVD diagnostic logs are stored in tables called WVDActivityV1_CL, WVDErrorV1_CL and WVDCheckpointV1_CL.

 

Example queries of WVD diagnostic logs

This section will give you some examples of the kind of queries you could run for your WVD environment. These queries can be turned into either analytics rules or hunting queries (covered later in this blog post).

 

This first example shows connection activities initiated by users with supported remote desktop clients:

WVDActivityV1_CL | where Type_s == "Connection" | join kind=leftouter (     WVDErrorV1_CL     | summarize Errors = makelist(pack('Time', Time_t, 'Code', ErrorCode_s , 'CodeSymbolic', ErrorCodeSymbolic_s, 'Message', ErrorMessage_s, 'ReportedBy', ReportedBy_s , 'Internal', ErrorInternal_s )) by ActivityId_g     ) on $left.Id_g  == $right.ActivityId_g  | join  kind=leftouter (     WVDCheckpointV1_CL     | summarize Checkpoints = makelist(pack('Time', Time_t, 'ReportedBy', ReportedBy_s, 'Name', Name_s, 'Parameters', Parameters_s) ) by ActivityId_g     ) on $left.Id_g  == $right.ActivityId_g |project-away ActivityId_g, ActivityId_g1

 

This next example query shows management activities by admins on tenants:

WVDActivityV1_CL | where Type_s == "Management" | join kind=leftouter (     WVDErrorV1_CL     | summarize Errors = makelist(pack('Time', Time_t, 'Code', ErrorCode_s , 'CodeSymbolic', ErrorCodeSymbolic_s, 'Message', ErrorMessage_s, 'ReportedBy', ReportedBy_s , 'Internal', ErrorInternal_s )) by ActivityId_g     ) on $left.Id_g  == $right.ActivityId_g  | join  kind=leftouter (     WVDCheckpointV1_CL     | summarize Checkpoints = makelist(pack('Time', Time_t, 'ReportedBy', ReportedBy_s, 'Name', Name_s, 'Parameters', Parameters_s) ) by ActivityId_g     ) on $left.Id_g  == $right.ActivityId_g |project-away ActivityId_g, ActivityId_g1

 

Querying Azure AD for the number of WVD sign ins per user:

 

SigninLogs | where TimeGenerated > ago(14d) | where AppDisplayName contains "Windows Virtual Desktop" | summarize count() by Identity | sort by count_ desc

 

Other useful queries in a WVD environment

This next set of queries lean towards the more operational side of WVD, but can be useful for exploring platform behavior and can be tuned to your specific environment. These queries could also be used to create Workbooks for monitoring your WVD environment.

 

Count of Host pools

WVDActivityV1_CL | summarize HostPools=dcount(SessionHostPoolName_s)

 

Unique users

WVDActivityV1_CL | summarize Sessions=dcount(UserName_s)

 

Session error

WVDActivityV1_CL | where (Error_Message_s contains "User Profile Disk setup failed for") or (Error_Message_s contains "There are currently no resources available to connect to") or (Error_Message_s contains "PreAuthLogonFailed") or (Error_Message_s contains "GatewayProtocolError") or (Error_Message_s contains "User Profile Disk setup failed at stage") or (Error_Message_s contains "Orchestration request failed Exception") or (Error_Message_s contains "failed") or (Error_Message_s contains "fail") or (Error_Message_s contains "One or more errors occurred") | project UserName=UserName_s, Error=Error_Message_s, Time=TimeGenerated | top 10 by UserName desc

 

Host pool usage

WVDActivityV1_CL | where ActivityType_s == "Connection" and Status_d == '1' | distinct StartTime_t, EndTime_t, UserName_s, Details_SessionHostName_s, Details_SessionHostPoolName_s | extend Seconds = datetime_diff('second', EndTime_t, StartTime_t) | extend Hours = Seconds / 3600.00 | summarize sum(Hours) by Details_SessionHostName_s

 

Usage over time

WVDActivityV1_CL | where ActivityType_s == "Connection" and Status_d == '1' | distinct StartTime_t, EndTime_t, UserName_s, Details_SessionHostName_s, Details_SessionHostPoolName_s | extend Seconds = datetime_diff('second', EndTime_t, StartTime_t) | extend Hours = Seconds / 3600.00 | summarize sum(Hours) by UserName_s, Host=Details_SessionHostName_s

 

Usage by user

WVDActivityV1_CL | where ActivityType_s == "Connection" and Status_d == '1' | distinct StartTime_t, EndTime_t, UserName_s, Details_SessionHostName_s, Details_SessionHostPoolName_s | extend Seconds = datetime_diff('second', EndTime_t, StartTime_t) | extend Hours = Seconds / 3600.00 | summarize sum(Hours) by UserName_s

 

CPU by VM

Perf | where ObjectName == "Processor" and InstanceName == "_Total" | summarize AvgCPU = avg(CounterValue) by Computer, bin(TimeGenerated, 1h)

 

Memory usage in the last 24 hours

Perf | where ObjectName == "Memory" and CounterName == "% Committed Bytes In Use" | summarize AvgRAM = toint(avg(CounterValue)) by Computer, bin(TimeGenerated, 1h)

 

WVD disk space

Perf | where ObjectName == "LogicalDisk" and CounterName == "% Free Space" | summarize avg(CounterValue) by Computer, bin(TimeGenerated, 1h)

 

 

Example detections for WVD environments

Access attempts to Windows Virtual Desktop by an unauthorized user, bad password, incorrect MFA or from a user account that does not exist.

 

let timeRange=ago(7d); SigninLogs | where TimeGenerated >= timeRange | where AppDisplayName contains "Windows Virtual Desktop" | where ResultType in ( "50126" , "50020", "50034", "50074", "50076", "50131") | extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails) | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city) | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), IPAddresses = makeset(IPAddress), DistinctIPCount = dcount(IPAddress), makeset(OS), makeset(Browser), makeset(City), AttemptCount = count() by UserDisplayName, UserPrincipalName, AppDisplayName, ResultType, ResultDescription, StatusCode, StatusDetails, Location, State | extend timestamp = StartTimeUtc, AccountCustomEntity = UserPrincipalName | sort by AttemptCount

 

User trying to log on to multiple host pools (more than the defined threshold of pools a user is expected to be a part of) within a one hour period.

 

let timeRange=ago(1h); let Threshold = 5; let Userlogintomultihostpool = WVDActivityV1_CL | where TimeGenerated >= timeRange | where Type_s == "Connection" | summarize dcount(SessionHostPoolName_s) by UserName_s | where dcount_SessionHostPoolName_s > Threshold | project UserName_s ; WVDActivityV1_CL | where TimeGenerated >= timeRange | where Type_s == "Connection" | where UserName_s in (Userlogintomultihostpool) | project SessionHostPoolName_s, UserName_s , ClientIPAddress_s , ClientType_s , TenantId_s , TimeGenerated , Id_g , Type_s , SessionHostIPAddress_s , SessionHostName_s , Outcome_s | sort by UserName_s asc

 

Azure Audit Logs provide a wealth of information on the operations on your Azure resources. This query will help you look at some relatively interesting operations related to Windows Virtual Desktop in your environment:

 

let timeRange=ago(7d); let RareOperations = dynamic(["Consent to application" , "Add delegated permission grant"]); AuditLogs | where TimeGenerated >= timeRange | extend ModProps = TargetResources.[0].modifiedProperties | extend IpAddress = iff(isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)), tostring(parse_json(tostring(InitiatedBy.user)).ipAddress), tostring(parse_json(tostring(InitiatedBy.app)).ipAddress)) | extend InitiatedBy = iff(isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)), tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName), tostring(parse_json(tostring(InitiatedBy.app)).displayName)) | extend TargetResourceName = tolower(tostring(TargetResources.[0].displayName)) | mvexpand ModProps | extend PropertyName = tostring(ModProps.displayName), newValue = replace("\"","",tostring(ModProps.newValue)) | where OperationName in (RareOperations) | where TargetResourceName contains "windows virtual desktop" | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Type, InitiatedBy, IpAddress, TargetResourceName, Category, OperationName, PropertyName, newValue, CorrelationId, Id

 

 

How are you monitoring your WVD environment? Whilst the queries included here are starting points for detection and hunting, we are sure that are plenty more ideas out there and we would love to see the community submitting things to our GitHub repo.

 

REMEMBER: these articles are REPUBLISHED. Your best bet to get a reply is to follow the link at the top of the post to the ORIGINAL post! BUT you're more than welcome to start discussions here:

This site uses Akismet to reduce spam. Learn how your comment data is processed.