Intune Custom Compliance for Cyber Essentials

Those outside of the UK may not have a scooby as to what Cyber Essentials actually is, so in short, it’s an assessed certification of a security standard provided by The National Cyber Security Centre, for prevention of Cyber attacks by protecting your organisation and devices within.
Why do I care about Cyber Essentials? Well there’s a good chunk of the security framework focusing on end user devices, of which the majority of controls can be achieved natively using Microsoft Intune Device Compliance policies.
Why am I writing this post if Microsoft Intune is already well positioned to secure devices to Cyber Essentials?
Well there are a few parts in the framework that Microsoft Intune doesn’t just pop out and fix things for you.
1 Cyber Essentials Requirements
To break down my understanding of Cyber Essentials for corporate owned devices, and the applicability to Device Compliance in Microsoft Intune, we need to ensure that a device is secured in the below broad ways:
- Built-in Guest account disabled - Remove and disable unnecessary user accounts (such as guest accounts and administrative accounts that won’t be used).
- Autoplay disabled - Disable any auto-run feature which allows file execution without user authorisation (such as when they are downloaded).
- Device Antivirus enabled - Prevent malware from running.
- Device Antivirus definitions up-to-date - Be updated in line with vendor recommendations.
- Device Firewall enabled - You must protect every device in scope with a correctly configured firewall (or network device with firewall functionality).
- Operating system patched - Be updated, including applying any manual configuration changes required to make the update effective, within 14 days of an update being released.
There are additional settings within the Cyber Essentials Requirements, that can be achieved using Configuration Profiles in Microsoft Intune, but our focus is using Device Compliance and Conditional Access to stop devices from accessing Entra authenticated services if they do not meet the above requirements.
2 Compliance Policies
So you might be thinking, “Well I’m using Windows Firewall, and Defender Antivirus, I’ll just use the native compliance checks and I’ll be on my way”, well bully for you, not everyone has jumped to Microsoft for everything. As we’ve seen with custom compliance for antivirus previously, there is merit to getting additional information out of installed security products on Windows devices.
This in conjunction with the other Cyber Essentials requirements which don’t have a nice interface to configure a Compliance Policy, means we have to lean into Custom Compliance to retrieve the information we care about, and check it against our known good policy check.
It’s worthwhile casting your eyes of the pre-requisites for Custom Compliance, as they’re not a one-size fits all setup, due to the need for an Entra Joined device, which is why we’re only focussing on corporate owned devices as part of this post.
Pre-requisites aside, let’s build ourselves a custom policy.
3 Custom Compliance Discovery Script
The basis for any and all custom compliance policies on Windows devices, is a discovery script written in PowerShell to gather information, putting that information into the correct JSON format, and have Microsoft Intune check the data against a validation JSON file you create.
To support the first part, we need our PowerShell script to punt data into the required JSON format, to do this we create a new PSObject
that we’ll use to add to during the discovery script.
$cyberEssentials = New-Object -TypeName PSObject
With the data receptacle (well that’s a horrible choice of word) now ready, we can continue to create the rest of our discovery script for the Cyber Essentials requirements.
3.1 Built-in Guest Account Discovery
We care only about the built-in Guest account, which should be disabled across your devices, and with a Configuration Profile disabling it there should be no leaks, however, we’re talking compliance here, so let’s use the fact the that built-in Guest account has an SID ending in 501
, and use the Win32_UserAccount
WMI object to pull back all user accounts that are like this value, and check whether they are enabled or disabled.
$guestAccount = Get-WmiObject Win32_UserAccount | Where-Object SID -Like '*501' | Select-Object Domain, Name, Disabled
[string]$guestAccountStatus = $guestAccount.Disabled
$cyberEssentials | Add-Member -MemberType NoteProperty -Name 'Guest account disabled' -Value $guestAccountStatus
All being well, our $cyberEssentials
variable should now look like:
Guest account disabled : True
3.2 Autoplay Discovery
To check if Autoplay is disabled for all drives, we can use the information in STIG Viewer to work out what registy key and value is used to turn off Autoplay.
The DWORD
value we’re after in key HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\
is NoDriveTypeAutoRun
with a value off 255
, so with a quick call to the registry, and this time a Try\Catch
setup to cover off if the value doesn’t exist entirely, we can pull back information around Autoplay.
Try {
$autorunState = Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\' -Name 'NoDriveTypeAutoRun'
if ($autorunState -eq 255) {
$autorunStatus = 'True'
}
else {
$autorunStatus = 'False'
}
}
Catch {
$autorunStatus = 'False'
}
$cyberEssentials | Add-Member -MemberType NoteProperty -Name 'Autorun disabled' -Value $autorunStatus
Checking our variable now, we have new information:
Guest account disabled : True
Autoplay disabled : False
I should probably speak with whoever manages my Windows device at this point đź‘€.
3.3 Antivirus Discovery
We’re no stranger to dealing with third-party antivirus compliance, so I’ve stolen from my own scripts, which makes a change, and updated it a little to just look at the realtime protection status, and the definition update status.
If you are using a third-party antivirus solution, you need to update the $avClient
variable with the name of the client, which can be captured by running the below on a target machine:
Get-WmiObject -Namespace 'root\SecurityCenter2' -Class AntiVirusProduct | Select-Object displayName
This will give us the displayName
of all security products registered under the AntiVirusProduct
class, and importantly what we need to update the $avClient
variable in our discovery script.
$avClient = 'AVG Antivirus' # Third-party Antivirus Client Name
if ($avClient) {
$avProduct = Get-WmiObject -Namespace 'root\SecurityCenter2' -Class AntiVirusProduct | Where-Object { $_.displayName -eq $avClient } | Select-Object -First 1
if ($avProduct) {
[string]$avProductState = [System.Convert]::ToString($avProduct.productState, 16).PadLeft(6, '0')
$avRealTimeProtection = $avProductState.Substring(2, 2)
$avDefinitions = $avProductState.Substring(4, 2)
$avRealTimeProtectionStatus = switch ($avRealTimeProtection) {
'00' { 'False' }
'01' { 'Expired' }
'10' { 'True' }
'11' { 'Snoozed' }
default { 'Unknown' }
}
$avDefinitionStatus = switch ($avDefinitions) {
'00' { 'True' }
'10' { 'False' }
default { 'Unknown' }
}
$cyberEssentials | Add-Member -MemberType NoteProperty -Name "$avClient real time protection enabled" -Value $avRealTimeProtectionStatus
$cyberEssentials | Add-Member -MemberType NoteProperty -Name "$avClient definitions up-to-date" -Value $avDefinitionStatus
}
else {
$cyberEssentials | Add-Member -MemberType NoteProperty -Name "$avClient real time protection enabled" -Value "Error: $avClient not product found"
$cyberEssentials | Add-Member -MemberType NoteProperty -Name "$avClient definitions up-to-date" -Value "Error: $avClient not product found"
}
}
else {
# Defender
$defenderStatus = Get-MpComputerStatus
$defenderAM = $defenderStatus.AMServiceEnabled
$defenderAS = $defenderStatus.AntispywareEnabled
$defenderAV = $defenderStatus.AntivirusEnabled
if ($defenderStatus.AntivirusSignatureAge -le 1) {
$defenderSig = 'True'
}
else {
$defenderSig = 'False'
}
$cyberEssentials | Add-Member -MemberType NoteProperty -Name 'Defender antimalware service enabled' -Value $defenderAM
$cyberEssentials | Add-Member -MemberType NoteProperty -Name 'Defender antispyware enabled' -Value $defenderAS
$cyberEssentials | Add-Member -MemberType NoteProperty -Name 'Defender antivirus enabled' -Value $defenderAV
$cyberEssentials | Add-Member -MemberType NoteProperty -Name 'Defender signatures up-to-date' -Value $defenderSig
}
Running this section of the discovery script now gives us a bit more information about AVG:
Guest account disabled : True
Autoplay disabled : False
AVG Antivirus real time protection enabled : True
AVG Antivirus definitions up-to-date : True
If you don’t specify the $avClient
variable, then the script will go have a peak at the state of your Defender setup instead:
Guest account disabled : True
Autoplay disabled : False
Defender antimalware service enabled : True
Defender antispyware enabled : True
Defender antivirus enabled : True
Defender signatures up-to-date : True
3.4 Firewall Discovery
So we can thank previous me for working out how to deal translate the data coming back from WMI for the AntiVirusProduct
class, as it translates to the FirewallProduct
class as well, which means we can use similar logic in our discovery script to understand whether the firewall is enabled or not.
Running the below will give us the information we need about our third-party firewall:
Get-WmiObject -Namespace 'root\SecurityCenter2' -Class FirewallProduct | Select-Object displayName
Now we can update the $fwClient
variable with the displayName
reported by the query above, in our script.
If there is no $fwClient
variable specified, the script will go and check each of the Windows firewall profiles and report back on their health.
$fwClient = 'ZoneAlarm NextGen Firewall' # Third-party Firewall Client Name
if ($fwClient) {
$fwProduct = Get-WmiObject -Namespace root\securityCenter2 -Class FirewallProduct | Where-Object { $_.displayName -eq $fwClient } | Select-Object -First 1
if ($fwProduct) {
[string]$fwProductState = [System.Convert]::ToString($fwProduct.ProductState, 16).padleft(6, '0')
$fwProtection = $fwProductState.substring(2, 2)
$fwProtectionStatus = switch ($fwProtection) {
'00' { 'False' }
'10' { 'True' }
default { 'Unknown' }
}
$cyberEssentials | Add-Member -MemberType NoteProperty -Name "$fwClient firewall enabled" -Value $fwProtectionStatus
}
else {
$cyberEssentials | Add-Member -MemberType NoteProperty -Name "$fwClient firewall enabled" -Value "Error: $fwClient product not found"
}
}
else {
# Defender Firewall Status
$fwProfiles = Get-NetFirewallProfile
foreach ($fwProfile in $fwProfiles) {
$fwStatus = $fwProfile.Enabled
$cyberEssentials | Add-Member -MemberType NoteProperty -Name "Windows Defender $($fwProfile.name) firewall enabled" -Value $fwStatus
}
}
For our third-party firewall client ZoneAlarm NextGen Firewall we now get something like the below:
Guest account disabled : True
Autoplay disabled : False
AVG Antivirus real time protection enabled : True
AVG Antivirus definitions up-to-date : True
ZoneAlarm NextGen Firewall firewall enabled : False
Or still just using good old Windows Firewall, we get an output like this:
Guest account disabled : True
Autoplay disabled : False
Defender antimalware service enabled : True
Defender antispyware enabled : True
Defender antivirus enabled : True
Defender signatures up-to-date : True
Windows Defender Domain firewall enabled : True
Windows Defender Private firewall enabled : True
Windows Defender Public firewall enabled : True
We’re almost there, and we’ve gathered some useful information so far.
3.5 Windows Updates Discovery
I’ve had to get creative with this one, as can’t rely on outside sources to understand whether the current build version of the device is the latest one, and deal with multiple feature update versions. So we’re looking at the LastWriteTimeUtc
from a list of files that get updated each month by Windows Updates. As updates are now cumulative, we just need to check when the last update was applied to the device, not which update was applied.
With this we can see when these files were last written to, which corresponds to when the last update was installed, so we need to make sure that the last time these files were written to, minus the day that the discovery script runs, is in the tolerance of having the latest update installed.
$updateTime = Get-Item @(
"${env:windir}\System32\ntoskrnl.exe",
"${env:windir}\System32\win32k.sys",
"${env:windir}\System32\win32kbase.sys",
"${env:windir}\System32\win32kfull.sys",
"${env:windir}\System32\ntdll.dll",
"${env:windir}\System32\USER32.dll",
"${env:windir}\System32\KERNEL32.dll",
"${env:windir}\System32\HAL.dll"
) | Measure-Object -Maximum LastWriteTimeUtc | Select-Object -ExpandProperty Maximum
$todayTime = Get-Date
If ((New-TimeSpan -Start $updateTime -End $todayTime).Days -le 31) {
$updateAge = 'True'
}
else {
$updateAge = 'False'
}
$cyberEssentials | Add-Member -MemberType NoteProperty -Name 'Windows operating system up-to-date' -Value $updateAge
Running (New-TimeSpan -Start $updateTime -End $todayTime).Days
on our test device, we can see that the days between today and the last update of those files gives us 232 days
which makes sense, as it’s a fresh out the box copy of Windows 10 22H2 with no updates installed.
Continuing with the script we now get the following added to the $cyberEssentials
object:
Guest account disabled : True
Autoplay disabled : False
AVG Antivirus real time protection enabled : True
AVG Antivirus definitions up-to-date : True
ZoneAlarm NextGen Firewall firewall enabled : False
Windows operating system up-to-date : False
With a final line in the script to output the $cyberEssentials
in the required format:
return $cyberEssentials | ConvertTo-Json -Compress
This gives us all the information we’re after from our Cyber Essentials based compliance policy, now we need to build our corresponding JSON answer file, and deploy the policy using Microsoft Intune.
4 JSON Validation
Now that we have our discovery script in hand, we need to create the JSON validation file, for our two main scenarios:
- AVG Antivirus and Zone Alarm Firewall
- Defender Antivirus and Windows Firewall
You can of course butcher modify the examples below based on your own scenarios, just make sure that the output coming from the discovery script matches the order of the JSON file.
SettingName
entries with your own third-party security software names, as the discovery script uses the $avClient
and $fwClient
variables to create the SettingName titles in the output of the discovery script.4.1 Third-Party JSON
Using both third-party antivirus and firewall solutions, our JSON file looks something like the below, being careful with case-sensitivity across the SettingName
values:
{
"Rules": [
{
"SettingName": "Built-in Guest account disabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://www.stigviewer.com/stig/windows_server_20122012_r2_member_server/2020-06-16/finding/V-1113",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "The built-in guest account is enabled",
"Description": "To comply with Cyber Essentials the built-in guest account must be disabled."
}
]
},
{
"SettingName": "Autoplay disabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://www.stigviewer.com/stig/windows_10/2017-12-01/finding/V-63673",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Autoplay for all drives is enabled",
"Description": "To comply with Cyber Essentials Autoplay for all devices must be disabled."
}
]
},
{
"SettingName": "AVG Antivirus real time protection enabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://support.avg.com/SupportArticleView?l=en&urlname=check-avg-antivirus-status",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "The AVG Antivirus real time protection is disabled",
"Description": "To comply with Cyber Essentials your device must be protected by an antivirus solution."
}
]
},
{
"SettingName": "AVG Antivirus definitions up-to-date",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://support.avg.com/SupportArticleView?l=en&urlname=update-avg-antivirus",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "AVG Antivirus definitions are out of date",
"Description": "To comply with Cyber Essentials your device must be protected by an up-to-date antivirus solution."
}
]
},
{
"SettingName": "ZoneAlarm NextGen Firewall firewall enabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://download.zonealarm.com/bin/inclient/ZA_HelpCenter/92606.htm",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "ZoneAlarm NextGen Firewall is disabled",
"Description": "To comply with Cyber Essentials all network profiles firewalls must be enabled."
}
]
},
{
"SettingName": "Windows operating system up-to-date",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://support.microsoft.com/en-gb/windows/get-the-latest-windows-update-7d20e88c-0568-483a-37bc-c3885390d212",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Windows operating system is out of date",
"Description": "To comply with Cyber Essentials your device must have the latest updates installed."
}
]
}
]
}
In my old age I’m getting kinder and kinder to end users, providing useful descriptions and even links to how to resolve the non-compliance issues within these policies. Not that the end user will look at them tbh, but still, I’ve done my part to make the Company Portal information look good.
4.2 Defender JSON
If you’re all over Defender and Microsoft security solutions, and who wouldn’t be, then the below JSON is for you:
{
"Rules": [
{
"SettingName": "Built-in Guest account disabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://www.stigviewer.com/stig/windows_server_20122012_r2_member_server/2020-06-16/finding/V-1113",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "The built-in guest account is enabled",
"Description": "To comply with Cyber Essentials the built-in guest account must be disabled."
}
]
},
{
"SettingName": "Autoplay disabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://www.stigviewer.com/stig/windows_10/2017-12-01/finding/V-63673",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Autoplay for all drives in enabled",
"Description": "To comply with Cyber Essentials Autoplay for all devices must be disabled."
}
]
},
{
"SettingName": "Defender antimalware service enabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://learn.microsoft.com/en-us/mem/intune/user-help/turn-on-defender-windows",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "The Defender Antimalware service is disabled",
"Description": "To comply with Cyber Essentials your device must be protected by an antivirus solution."
}
]
},
{
"SettingName": "Defender antispyware enabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://learn.microsoft.com/en-us/mem/intune/user-help/turn-on-defender-windows",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Defender Antispyware service is disabled",
"Description": "To comply with Cyber Essentials your device must be protected by an antivirus solution."
}
]
},
{
"SettingName": "Defender antivirus enabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://learn.microsoft.com/en-us/mem/intune/user-help/turn-on-defender-windows",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Defender Antivirus is disabled",
"Description": "To comply with Cyber Essentials your device must be protected by an antivirus solution."
}
]
},
{
"SettingName": "Defender signatures up-to-date",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://learn.microsoft.com/en-us/mem/intune/user-help/turn-on-defender-windows",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Defender Antivirus signatures are out of date",
"Description": "To comply with Cyber Essentials your device must be protected by an up-to-date antivirus solution."
}
]
},
{
"SettingName": "Windows Defender Domain firewall enabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://learn.microsoft.com/en-us/mem/intune/user-help/you-need-to-enable-defender-firewall-windows",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Windows Defender Firewall Domain Profile is disabled",
"Description": "To comply with Cyber Essentials all network profiles firewalls must be enabled."
}
]
},
{
"SettingName": "Windows Defender Private firewall enabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://learn.microsoft.com/en-us/mem/intune/user-help/you-need-to-enable-defender-firewall-windows",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Windows Defender Firewall Private Profile is disabled",
"Description": "To comply with Cyber Essentials all network profiles firewalls must be enabled."
}
]
},
{
"SettingName": "Windows Defender Public firewall enabled",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://learn.microsoft.com/en-us/mem/intune/user-help/you-need-to-enable-defender-firewall-windows",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Windows Defender Firewall Public Profile is disabled",
"Description": "To comply with Cyber Essentials all network profiles firewalls must be enabled."
}
]
},
{
"SettingName": "Windows operating system up-to-date",
"Operator": "IsEquals",
"DataType": "String",
"Operand": "True",
"MoreInfoUrl": "https://support.microsoft.com/en-gb/windows/get-the-latest-windows-update-7d20e88c-0568-483a-37bc-c3885390d212",
"RemediationStrings": [
{
"Language": "en_US",
"Title": "Windows operating system is out of date",
"Description": "To comply with Cyber Essentials your device must have the latest updates installed."
}
]
}
]
}
With a validation file saved, we can now move to the final step, and the whole point, creating Compliance Policies that align with Cyber Essentials.
5 Deploying Custom Compliance
After a whole lot of PowerShell and JSON, we’re now at the point where we can smush it all together to get our desired outcome, a Device Compliance Policy to check for Cyber Essentials requirements, and if we’re using compliance based Conditional Access policy, a true way to say GTFO if you’re not secure (your device, not your self esteem).
5.1 Compliance Scripts
So off into the safe space that is Microsoft Intune to create a Cyber Essentials based Compliance Policy, but before that, we’d better add our discovery script otherwise we’re not going to make any progress.
Navigate to Devices > Compliance > Scripts and select the happy looking Add+ button, choosing Windows 10 and later:
Give the new script a name, (check me out with a naming convention at last):
Select Next, and the paste in the discovery script, (honestly, an upload would be much better here but whatever), here you can update the $avClient
and $fwClient
variables if you’re using third-party solutions and haven’t updated the script previously:
Run script in 64 bit PowerShell Host
is set to Yes, leave everything else as is.Once you’ve selected Create you might need to wait a few minutes for the new script to appear in the pane.
5.2 Compliance Policy
Go create yourself a new Windows 10 and later compliance policy, and stick with whatever naming convention works for you, selecting Next after you’ve done so.
Expand Custom Compliance and select Require and select the discovery script that we uploaded in the previous step.
Once you’ve selected the discovery script we created previously, we now need to upload the JSON validation file, and in doing so, if all went well, Microsoft Intune will display the settings the custom compliance section will look for:
We’re now in a position to configure the actions for non-compliance, and the assignments. After that, we just need to wait for our devices to check in, evaluate the policy and report back to Microsoft Intune their results.
5.3 Compliance Results
After waiting for our device to evaluate the compliance policy, at some point, maybe 20 minutes, we should start to get data back into Microsoft Intune showing the sorry state the device is in:
Checking on our test device, with the Zone Alarm firewall and AVG antivirus protection disabled, we can see that the Company Portal is reporting the correct non-compliance status:
By expanding the non-compliance notifications, we can make sure that our configured JSON validation settings are displaying nice messages to our end users:
So there we have it, the compliance policy doing what it’s good at.
6 Summary
This Custom Compliance policy isn’t going to be for everyone, but for those that are looking to align with Cyber Essentials and a zero trust approach based on these requirements, it’s a must have use of Microsoft Intune.
Even if you’re not jumping on the shiny new security badge bandwagon, there isn’t anything stopping you taking these example compliance settings and building your own based upon them, to add to, or improve upon your existing compliance policies for corporate owned Windows devices.
Just remember that there are still some limitations with Custom Compliance policies that you need to be aware of before just punting them into Microsoft Intune blindly.