mirror of
https://github.com/EvotecIT/GPOZaurr.git
synced 2026-07-28 12:49:03 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98e93aa07d | |||
| 707c0ed4b2 | |||
| 3a69d14354 |
@@ -1,5 +1,12 @@
|
||||
# GPOZaurr Release History
|
||||
|
||||
## Unreleased
|
||||
- Fix `WindowsDefender` content detection for newer ADMX categories by supporting both:
|
||||
- `Windows Components/Windows Defender*`
|
||||
- `Windows Components/Microsoft Defender Antivirus*`
|
||||
- Fix `WindowsDefenderExploitGuard` detection for legacy/new category naming variants.
|
||||
- Add Defender registry fallback parsing (`SOFTWARE\Microsoft\Windows Defender*`) so Defender settings that land in `RegistrySettings` are still surfaced in `WindowsDefender` report output. [#81](https://github.com/EvotecIT/GPOZaurr/issues/81)
|
||||
|
||||
## 1.1.9 - 2024.12.02
|
||||
- Fixes `Invoke-GPOZaurr` when using SplitReports without path [#58](https://github.com/EvotecIT/GPOZaurr/issues/58)
|
||||
|
||||
|
||||
@@ -92,7 +92,26 @@
|
||||
|
||||
#>
|
||||
if ($Value.Value.Element) {
|
||||
$Settings["$SubName"] = $Value.Value.Element.Data -join '; '
|
||||
[Array] $ElementValues = foreach ($Element in @($Value.Value.Element)) {
|
||||
if (-not $Element) {
|
||||
continue
|
||||
}
|
||||
$ElementName = [string] $Element.Name
|
||||
$ElementData = [string] $Element.Data
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ElementName) -and ($ElementData -eq '' -or $ElementData -eq '0')) {
|
||||
$ElementName
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($ElementName) -and -not [string]::IsNullOrWhiteSpace($ElementData) -and $ElementName -ne $ElementData) {
|
||||
"$ElementName ($ElementData)"
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($ElementData)) {
|
||||
$ElementData
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($ElementName)) {
|
||||
$ElementName
|
||||
}
|
||||
}
|
||||
if ($ElementValues.Count -gt 0) {
|
||||
$Settings["$SubName"] = $ElementValues -join '; '
|
||||
}
|
||||
} elseif ($null -eq $Value.Value.Name) {
|
||||
# Shouldn't happen but lets see
|
||||
Write-Verbose "Tracking $Value"
|
||||
@@ -174,7 +193,26 @@
|
||||
|
||||
#>
|
||||
if ($Value.Value.Element) {
|
||||
$CreateGPO["$SubName"] = $Value.Value.Element.Data -join '; '
|
||||
[Array] $ElementValues = foreach ($Element in @($Value.Value.Element)) {
|
||||
if (-not $Element) {
|
||||
continue
|
||||
}
|
||||
$ElementName = [string] $Element.Name
|
||||
$ElementData = [string] $Element.Data
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ElementName) -and ($ElementData -eq '' -or $ElementData -eq '0')) {
|
||||
$ElementName
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($ElementName) -and -not [string]::IsNullOrWhiteSpace($ElementData) -and $ElementName -ne $ElementData) {
|
||||
"$ElementName ($ElementData)"
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($ElementData)) {
|
||||
$ElementData
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($ElementName)) {
|
||||
$ElementName
|
||||
}
|
||||
}
|
||||
if ($ElementValues.Count -gt 0) {
|
||||
$CreateGPO["$SubName"] = $ElementValues -join '; '
|
||||
}
|
||||
} elseif ($null -eq $Value.Value.Name) {
|
||||
# Shouldn't happen but lets see
|
||||
Write-Verbose "Tracking $Value"
|
||||
@@ -204,4 +242,4 @@
|
||||
#}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
function ConvertTo-XMLRegistryDefenderOnReport {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Converts Defender-related raw registry settings from the RegistrySettings report.
|
||||
|
||||
.DESCRIPTION
|
||||
This function is used as a fallback for Defender settings that are not exposed as
|
||||
policy categories but are still present in RegistrySettings output.
|
||||
|
||||
.PARAMETER GPO
|
||||
RegistrySettings report object generated by ConvertTo-XMLRegistrySettings -SingleObject.
|
||||
#>
|
||||
[cmdletBinding()]
|
||||
param(
|
||||
[PSCustomObject] $GPO
|
||||
)
|
||||
|
||||
[Array] $RegistrySettings = @()
|
||||
if ($GPO.Settings) {
|
||||
$RegistrySettings = $GPO.Settings
|
||||
} elseif ($GPO.DataSet) {
|
||||
# This path supports direct use from dictionary code where RegistrySettings is not requested explicitly.
|
||||
[Array] $DataSet = $GPO.DataSet
|
||||
if ($DataSet.Count -gt 0 -and (
|
||||
$DataSet[0].PSObject.Properties.Name -contains 'Properties' -or
|
||||
$DataSet[0].PSObject.Properties.Name -contains 'Registry' -or
|
||||
$DataSet[0].PSObject.Properties.Name -contains 'Collection'
|
||||
)
|
||||
) {
|
||||
$RegistrySettings = Get-XMLNestedRegistry -GPO $GPO -DataSet $GPO.DataSet
|
||||
}
|
||||
}
|
||||
|
||||
$UsedNames = [System.Collections.Generic.List[string]]::new()
|
||||
$CreateGPO = [ordered] @{
|
||||
DisplayName = $GPO.DisplayName
|
||||
DomainName = $GPO.DomainName
|
||||
GUID = $GPO.GUID
|
||||
GpoType = $GPO.GpoType
|
||||
}
|
||||
$DefenderSettings = 0
|
||||
|
||||
foreach ($Registry in $RegistrySettings) {
|
||||
if ($Registry.Key -like 'SOFTWARE\Microsoft\Windows Defender*') {
|
||||
$DefenderSettings++
|
||||
$SettingName = if ($Registry.Name) { $Registry.Name } else { $Registry.Key }
|
||||
$PropertyName = Format-ToTitleCase -Text $SettingName -RemoveWhiteSpace -RemoveChar ',', '-', "'", '\(', '\)', ':'
|
||||
|
||||
if ($PropertyName -in $UsedNames) {
|
||||
$UsedNames.Add($PropertyName)
|
||||
$TimesUsed = ($UsedNames | Group-Object | Where-Object { $_.Name -eq $PropertyName }).Count
|
||||
$PropertyName = -join ($PropertyName, "$TimesUsed")
|
||||
} else {
|
||||
$UsedNames.Add($PropertyName)
|
||||
}
|
||||
|
||||
$SettingValue = $Registry.Value
|
||||
if ($null -eq $SettingValue -or "$SettingValue" -eq '') {
|
||||
$SettingValue = $Registry.Name
|
||||
}
|
||||
$CreateGPO[$PropertyName] = $SettingValue
|
||||
|
||||
# Some Defender data can be encoded in either value or value-name depending on setting type.
|
||||
if ($Registry.Name -and "$SettingValue" -ne "$($Registry.Name)") {
|
||||
$CreateGPO["$($PropertyName)ValueName"] = $Registry.Name
|
||||
}
|
||||
if ($Registry.Key) {
|
||||
$CreateGPO["$($PropertyName)RegistryKey"] = $Registry.Key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($DefenderSettings -gt 0) {
|
||||
$CreateGPO['Linked'] = $GPO.Linked
|
||||
$CreateGPO['LinksCount'] = $GPO.LinksCount
|
||||
$CreateGPO['Links'] = $GPO.Links
|
||||
[PSCustomObject] $CreateGPO
|
||||
}
|
||||
}
|
||||
@@ -1169,13 +1169,26 @@
|
||||
Category = 'RegistrySettings'
|
||||
Settings = 'Policy'
|
||||
}
|
||||
@{
|
||||
Category = 'RegistrySettings'
|
||||
Settings = 'RegistrySettings'
|
||||
}
|
||||
)
|
||||
GPOPath = @(
|
||||
'Policies -> Administrative Templates -> Windows Components/Windows Defender'
|
||||
'Policies -> Administrative Templates -> Windows Components/Microsoft Defender Antivirus'
|
||||
)
|
||||
GPOPath = 'Policies -> Administrative Templates -> Windows Components/Windows Defender'
|
||||
Code = {
|
||||
ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Windows Defender*'
|
||||
@(
|
||||
ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Windows Defender*', 'Windows Components/Microsoft Defender Antivirus*'
|
||||
ConvertTo-XMLRegistryDefenderOnReport -GPO $GPO
|
||||
) | Where-Object { $_ }
|
||||
}
|
||||
CodeSingle = {
|
||||
ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Windows Defender*' -SingleObject
|
||||
@(
|
||||
ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Windows Defender*', 'Windows Components/Microsoft Defender Antivirus*' -SingleObject
|
||||
ConvertTo-XMLRegistryDefenderOnReport -GPO $GPO
|
||||
) | Where-Object { $_ }
|
||||
}
|
||||
}
|
||||
WindowsDefenderExploitGuard = @{
|
||||
@@ -1186,12 +1199,15 @@
|
||||
Settings = 'Policy'
|
||||
}
|
||||
)
|
||||
GPOPath = 'Policies -> Administrative Templates -> Windows Components/Microsoft Defender Antivirus/Microsoft Defender Exploit Guard'
|
||||
GPOPath = @(
|
||||
'Policies -> Administrative Templates -> Windows Components/Windows Defender/Windows Defender Exploit Guard'
|
||||
'Policies -> Administrative Templates -> Windows Components/Microsoft Defender Antivirus/Microsoft Defender Exploit Guard'
|
||||
)
|
||||
Code = {
|
||||
ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Microsoft Defender Antivirus/Microsoft Defender Exploit Guard*'
|
||||
ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Windows Defender*/Windows Defender Exploit Guard*', 'Windows Components/Microsoft Defender Antivirus/Microsoft Defender Exploit Guard*', 'Windows Components/Microsoft Defender Antivirus/Windows Defender Exploit Guard*'
|
||||
}
|
||||
CodeSingle = {
|
||||
ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Microsoft Defender Antivirus/Microsoft Defender Exploit Guard*' -SingleObject
|
||||
ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Windows Defender*/Windows Defender Exploit Guard*', 'Windows Components/Microsoft Defender Antivirus/Microsoft Defender Exploit Guard*', 'Windows Components/Microsoft Defender Antivirus/Windows Defender Exploit Guard*' -SingleObject
|
||||
}
|
||||
}
|
||||
# WindowsFirewall = @{
|
||||
@@ -1428,4 +1444,4 @@
|
||||
ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Windows Update*', 'Windows Components/Delivery Optimization*' -SingleObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +287,39 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($Output['Reports']['WindowsDefender']) {
|
||||
# Defender can be represented by policy categories and raw registry settings.
|
||||
# Merge entries per GPO so fallback fields do not create duplicated-looking rows.
|
||||
$MergedWindowsDefender = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
$GroupedWindowsDefender = $Output['Reports']['WindowsDefender'] | Group-Object -Property DisplayName, DomainName, GUID, GpoType
|
||||
foreach ($Group in $GroupedWindowsDefender) {
|
||||
$MergedObject = [ordered] @{}
|
||||
foreach ($Entry in $Group.Group) {
|
||||
foreach ($Property in $Entry.PSObject.Properties) {
|
||||
if (-not $MergedObject.Contains($Property.Name)) {
|
||||
$MergedObject[$Property.Name] = $Property.Value
|
||||
} else {
|
||||
$CurrentValue = $MergedObject[$Property.Name]
|
||||
$CurrentEmpty = $null -eq $CurrentValue -or ($CurrentValue -is [string] -and $CurrentValue -eq '')
|
||||
$NewEmpty = $null -eq $Property.Value -or ($Property.Value -is [string] -and $Property.Value -eq '')
|
||||
if ($CurrentEmpty -and -not $NewEmpty) {
|
||||
$MergedObject[$Property.Name] = $Property.Value
|
||||
} elseif (-not $CurrentEmpty -and -not $NewEmpty -and "$CurrentValue" -ne "$($Property.Value)") {
|
||||
$PropertyCounter = 2
|
||||
$AlternativeName = -join ($Property.Name, "$PropertyCounter")
|
||||
while ($MergedObject.Contains($AlternativeName)) {
|
||||
$PropertyCounter++
|
||||
$AlternativeName = -join ($Property.Name, "$PropertyCounter")
|
||||
}
|
||||
$MergedObject[$AlternativeName] = $Property.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$MergedWindowsDefender.Add([PSCustomObject] $MergedObject)
|
||||
}
|
||||
$Output['Reports']['WindowsDefender'] = $MergedWindowsDefender
|
||||
}
|
||||
# Normalize - meaning that before we return each GPO report we make sure that each entry has the same column names regardless which one is first.
|
||||
# Normally if you would have a GPO with just 2 entries for given subject (say LAPS), and then another GPO having 5 settings for the same type
|
||||
# and you would display them one after another - all entries would be shown using first object which has less properties then 2nd or 3rd object
|
||||
@@ -352,4 +385,4 @@
|
||||
|
||||
$Script:GPODitionary.Keys | Sort-Object | Where-Object { $_ -like "*$wordToComplete*" }
|
||||
}
|
||||
Register-ArgumentCompleter -CommandName Invoke-GPOZaurrContent -ParameterName Type -ScriptBlock $SourcesAutoCompleter
|
||||
Register-ArgumentCompleter -CommandName Invoke-GPOZaurrContent -ParameterName Type -ScriptBlock $SourcesAutoCompleter
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
Describe 'Defender content detection' {
|
||||
BeforeAll {
|
||||
Import-Module $PSScriptRoot\..\*.psd1 -Force
|
||||
}
|
||||
|
||||
It 'WindowsDefender dictionary supports old and new categories' {
|
||||
InModuleScope GPOZaurr {
|
||||
$Entry = $Script:GPODitionary['WindowsDefender']
|
||||
$Entry.GPOPath | Should -Contain 'Policies -> Administrative Templates -> Windows Components/Windows Defender'
|
||||
$Entry.GPOPath | Should -Contain 'Policies -> Administrative Templates -> Windows Components/Microsoft Defender Antivirus'
|
||||
($Entry.Types | Where-Object { $_.Category -eq 'RegistrySettings' -and $_.Settings -eq 'RegistrySettings' }).Count | Should -BeGreaterOrEqual 1
|
||||
$Entry.Code.ToString() | Should -Match 'ConvertTo-XMLRegistryDefenderOnReport'
|
||||
}
|
||||
}
|
||||
|
||||
It 'WindowsDefenderExploitGuard supports category variants' {
|
||||
InModuleScope GPOZaurr {
|
||||
$Entry = $Script:GPODitionary['WindowsDefenderExploitGuard']
|
||||
$Entry.GPOPath | Should -Contain 'Policies -> Administrative Templates -> Windows Components/Windows Defender/Windows Defender Exploit Guard'
|
||||
$Entry.GPOPath | Should -Contain 'Policies -> Administrative Templates -> Windows Components/Microsoft Defender Antivirus/Microsoft Defender Exploit Guard'
|
||||
$Entry.Code.ToString() | Should -Match 'Windows Components/Microsoft Defender Antivirus/Windows Defender Exploit Guard\*'
|
||||
}
|
||||
}
|
||||
|
||||
It 'ConvertTo-XMLRegistryDefenderOnReport returns only Defender registry settings' {
|
||||
InModuleScope GPOZaurr {
|
||||
$GPO = [PSCustomObject] @{
|
||||
DisplayName = 'Test Defender GPO'
|
||||
DomainName = 'contoso.com'
|
||||
GUID = '11111111-1111-1111-1111-111111111111'
|
||||
GpoType = 'Computer'
|
||||
Linked = $true
|
||||
LinksCount = 1
|
||||
Links = @('OU=Workstations,DC=contoso,DC=com')
|
||||
Settings = @(
|
||||
[PSCustomObject] @{
|
||||
Hive = 'HKEY_LOCAL_MACHINE'
|
||||
Key = 'SOFTWARE\Microsoft\Windows Defender\MpEngine'
|
||||
Name = 'MpFolderScanThreadCount'
|
||||
Type = 'REG_DWORD'
|
||||
Value = '4'
|
||||
Changed = [datetime] '2026-02-18T11:15:00'
|
||||
Filters = $null
|
||||
}
|
||||
[PSCustomObject] @{
|
||||
Hive = 'HKEY_LOCAL_MACHINE'
|
||||
Key = 'SOFTWARE\Contoso\Other'
|
||||
Name = 'Setting'
|
||||
Type = 'REG_SZ'
|
||||
Value = 'Value'
|
||||
Changed = [datetime] '2026-02-18T11:15:00'
|
||||
Filters = $null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
[Array] $Result = ConvertTo-XMLRegistryDefenderOnReport -GPO $GPO
|
||||
$Result.Count | Should -Be 1
|
||||
$Result[0].MpFolderScanThreadCount | Should -Be '4'
|
||||
$Result[0].MpFolderScanThreadCountRegistryKey | Should -Be 'SOFTWARE\Microsoft\Windows Defender\MpEngine'
|
||||
}
|
||||
}
|
||||
|
||||
It 'ConvertTo-XMLRegistryDefenderOnReport supports raw DataSet input' {
|
||||
InModuleScope GPOZaurr {
|
||||
$GPO = [PSCustomObject] @{
|
||||
DisplayName = 'Test Defender GPO'
|
||||
DomainName = 'contoso.com'
|
||||
GUID = '11111111-1111-1111-1111-111111111111'
|
||||
GpoType = 'Computer'
|
||||
Linked = $true
|
||||
LinksCount = 1
|
||||
Links = @('OU=Workstations,DC=contoso,DC=com')
|
||||
DataSet = ([xml] @"
|
||||
<Root>
|
||||
<Registry changed='2026-02-18T11:15:00' disabled='0'>
|
||||
<Properties action='U' hive='HKEY_LOCAL_MACHINE' key='SOFTWARE\Microsoft\Windows Defender\MpEngine' name='MpFolderScanThreadCount' type='REG_DWORD' value='4' />
|
||||
</Registry>
|
||||
</Root>
|
||||
"@).Root.Registry
|
||||
}
|
||||
|
||||
[Array] $Result = ConvertTo-XMLRegistryDefenderOnReport -GPO $GPO
|
||||
$Result.Count | Should -Be 1
|
||||
$Result[0].MpFolderScanThreadCount | Should -Be '4'
|
||||
$Result[0].MpFolderScanThreadCountRegistryKey | Should -Be 'SOFTWARE\Microsoft\Windows Defender\MpEngine'
|
||||
}
|
||||
}
|
||||
|
||||
It 'ConvertTo-XMLGenericPolicy prefers list item names over zero-only data' {
|
||||
InModuleScope GPOZaurr {
|
||||
[xml] $ListValue = @"
|
||||
<Value>
|
||||
<Element>
|
||||
<Name>C:\PathOne</Name>
|
||||
<Data>0</Data>
|
||||
</Element>
|
||||
<Element>
|
||||
<Name>D:\PathTwo</Name>
|
||||
<Data>0</Data>
|
||||
</Element>
|
||||
</Value>
|
||||
"@
|
||||
|
||||
$GPO = [PSCustomObject] @{
|
||||
DisplayName = 'Test Defender GPO'
|
||||
DomainName = 'contoso.com'
|
||||
GUID = '11111111-1111-1111-1111-111111111111'
|
||||
GpoType = 'Computer'
|
||||
Linked = $true
|
||||
LinksCount = 1
|
||||
Links = @('OU=Workstations,DC=contoso,DC=com')
|
||||
DataSet = @(
|
||||
[PSCustomObject] @{
|
||||
Category = 'Windows Components/Microsoft Defender Antivirus/Exclusions'
|
||||
Name = 'Path Exclusions'
|
||||
State = 'Enabled'
|
||||
ListBox = @(
|
||||
[PSCustomObject] @{
|
||||
Name = 'Path Exclusions'
|
||||
Value = $ListValue.Value
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$Result = ConvertTo-XMLGenericPolicy -GPO $GPO -Category 'Windows Components/Microsoft Defender Antivirus*'
|
||||
$Result.PathExclusionsPathExclusions | Should -Match 'C:\\PathOne'
|
||||
$Result.PathExclusionsPathExclusions | Should -Match 'D:\\PathTwo'
|
||||
$Result.PathExclusionsPathExclusions | Should -Not -Be '0; 0'
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user