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


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.



 

Leave a Reply

Your email address will not be published. Required fields are marked *

*

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