Refine Defender output merge and list parsing for issue #81 feedback

This commit is contained in:
Przemysław Kłys
2026-02-24 14:28:47 +01:00
parent 707c0ed4b2
commit 98e93aa07d
4 changed files with 164 additions and 25 deletions
+41 -3
View File
@@ -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 @@
#}
}
}
}
}
@@ -31,25 +31,49 @@ function ConvertTo-XMLRegistryDefenderOnReport {
}
}
$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*') {
[PSCustomObject] [ordered] @{
DisplayName = $GPO.DisplayName
DomainName = $GPO.DomainName
GUID = $GPO.GUID
GpoType = $GPO.GpoType
FallbackSource = 'RegistrySettings'
Hive = $Registry.Hive
Key = $Registry.Key
Name = $Registry.Name
Type = $Registry.Type
Value = $Registry.Value
Changed = $Registry.Changed
Filters = $Registry.Filters
Linked = $GPO.Linked
LinksCount = $GPO.LinksCount
Links = $GPO.Links
$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
}
}
+34 -1
View File
@@ -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
+49 -5
View File
@@ -56,9 +56,8 @@ Describe 'Defender content detection' {
[Array] $Result = ConvertTo-XMLRegistryDefenderOnReport -GPO $GPO
$Result.Count | Should -Be 1
$Result[0].FallbackSource | Should -Be 'RegistrySettings'
$Result[0].Key | Should -Be 'SOFTWARE\Microsoft\Windows Defender\MpEngine'
$Result[0].Name | Should -Be 'MpFolderScanThreadCount'
$Result[0].MpFolderScanThreadCount | Should -Be '4'
$Result[0].MpFolderScanThreadCountRegistryKey | Should -Be 'SOFTWARE\Microsoft\Windows Defender\MpEngine'
}
}
@@ -83,8 +82,53 @@ Describe 'Defender content detection' {
[Array] $Result = ConvertTo-XMLRegistryDefenderOnReport -GPO $GPO
$Result.Count | Should -Be 1
$Result[0].Key | Should -Be 'SOFTWARE\Microsoft\Windows Defender\MpEngine'
$Result[0].Name | Should -Be 'MpFolderScanThreadCount'
$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'
}
}
}