Contents

Defender for Endpoint - Did the Antivirus scan complete?

Microsoft Defender for Endpoint has great automation capabilities and you can alert using custom detection rules. Put that together and you can trigger many on-client events using those custom detection. This could be to isolate the device from the network, start an automated investigation, collect an investigation package, restrict app execution or run an full antivirus scan on the device in question.

But how do you know if Microsoft Defender Antivirus has finished to scan the device?

Just search for “AntivirusScan” in the device timeline.

You should find at least two events after you triggered the AV scan:

  • Event of type [AntivirusScanResponse] observed on device
    This is the indication that the agent has received the command to scan the device.
  • Windows Defender Antivirus scan completed
    And this event is triggered when the Microsoft Defender Antivirus agent has completed the job.

As you can see in the following screenshot the “AntivirusScanResponse” event is not always present before a AV scan has completed. This is because scheduled scans do not have this trigger and just the final scan result is reported.

/en/antivirus-scan-complete/images/Timeline.png

Advanced Hunting

The first event is nothing I could pinpoint using advanced hunting. The second one can be found in the DeviceEvents table.

DeviceEvents
| where ActionType contains "AntivirusScan"

As of now the ActionType can have the following values regarding AV scans.

  • AntivirusScanCompleted
  • AntivirusScanCancelled
  • AntivirusScanFailed

There is one additional scan type SafeDocFileScan that is related to the Safe Documents in Microsoft 365 feature and not scope of this blog post.

As far as I could tell this event is only triggered on the currently supported Windows operating systems. Linux and MacOS devices did not report this event.

List all AV scans

Use the following query to get a complete list of AV scans in your environment, including the triggering user and the type of the scan (Quick or Full).

DeviceEvents
| where ActionType contains "AntivirusScan"
| extend AdditionalFields = todynamic(AdditionalFields)
| extend ScanType = AdditionalFields.["ScanTypeIndex"], StartedBy= AdditionalFields.["User"]
| project Timestamp, DeviceName, ActionType, ScanType, StartedBy

/en/antivirus-scan-complete/images/ScanResults.png

Based on the StartedBy column you can differentiate between scan that were scheduled or triggered through MDE.

StartedBy Scan reason
NETWORK SERVICE Local scheduler
SYSTEM Microsoft Defender for Endpoint trigger

The username seems to be localized on the client side, so your environment might differ a little bit.

Report devices without current AV scan results

With this knowledge we can easily create a report, which devices did or did not complete an AV scan in the last two weeks.

 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
// Devices without successful AV scan in the last n days
// As of 27.01.2022 only the following platforms are support
// Windows10, Windows10WVD, Windows11, WindowsServer2012R2, WindowsServer2016, WindowsServer2019, WindowsServer2022
let Timerange = 14d;
DeviceInfo
| where OnboardingStatus == "Onboarded"
| where isnotempty( OSVersion)
| where Timestamp > ago(Timerange)
| summarize LastSeen = arg_max(Timestamp, *) by DeviceId
| extend LastSuccessfulAVScan = strcat("Not in the last ",format_timespan(Timerange,'d')," days")
| project LastSeen, DeviceId, DeviceName, MachineGroup, OSPlatform, OSVersion, DeviceType, LastSuccessfulAVScan, JoinType
// use rightsemi to return all devices that had a successful AV scan in the last n days
// use leftanti to return all devices that NOT had a successful AV scan in the last n days
| join kind=leftanti (
    DeviceEvents
    | where ActionType == "AntivirusScanCompleted"
    | where Timestamp > ago(Timerange)
    | summarize LastSuccessfulAVScan = max(Timestamp) by DeviceName, DeviceId
    | join kind=innerunique (
        DeviceInfo
        | where isnotempty( OSVersion )
    ) on DeviceId
    | summarize LastSeen = arg_max(Timestamp,*) by DeviceName
    | project LastSeen, DeviceId, DeviceName, MachineGroup, OSPlatform, OSVersion, DeviceType, LastSuccessfulAVScan, JoinType
) on DeviceId
| where OSPlatform in ("Windows10","Windows10WVD","Windows11","WindowsServer2012R2","WindowsServer2016","WindowsServer2019","WindowsServer2022")
| sort by DeviceType, MachineGroup, OSPlatform

I highlighted line 12-14 because this is where you can change what kind of result you prefer. When you use the join operator with kind=rightsemi the report will include all devices that did a successful AV scan including the date of the last scan.

Change this to kind=leftanti and you will get the exact opposite. The results now include only devices that had no successful AV scan in the last 14 days.

And of course you can change the timerange to fit your needs.

#365DaysofKQL and #MustLearnKQL

If you like to learn more about KQL/Kusto and interested in great examples check out #365DaysofKQL by Matt Zorich (@reprise_99).

Also visit aka.ms/MustLearnKQL an initative by Rod Trent (@rodtrent)

I added my query to the repository of Matt.