Compare commits

...

12 Commits

Author SHA1 Message Date
Przemysław Kłys 98e93aa07d Refine Defender output merge and list parsing for issue #81 feedback 2026-02-24 14:28:47 +01:00
Przemysław Kłys 707c0ed4b2 Address PR feedback: make Defender fallback work without RegistrySettings type 2026-02-18 11:48:10 +01:00
Przemysław Kłys 3a69d14354 Fix Defender policy detection for new ADMX paths and add registry fallback (#81) 2026-02-18 11:39:31 +01:00
Przemysław Kłys 95789953a8 fix: 🐛 update copyright year and module versions
* Updated copyright year from 2024 to 2025.
* Incremented `ModuleVersion` from `1.1.9` to `1.1.10`.
* Updated dependencies' module versions:
  - `PSWriteColor` from `1.0.1` to `1.0.3`
  - `PSSharedGoods` from `0.0.301` to `0.0.312`
  - `ADEssentials` from `0.0.226` to `0.0.267`
* Refactored `Foreach` to `foreach` for consistency in `GPOZaurr.psm1`.
2025-12-05 08:45:58 +01:00
Przemysław Kłys 927ae5cd15 fix: 🐛 correct typo in 'Blocked Inhertiance' to 'Blocked Inheritance'
* Updated comments in `Example-36-FindBlockedInheritance.ps1` for clarity.
* Fixed spelling error in `Invoke.GPOZaurrBlockedInheritance.ps1`.
2025-12-04 08:33:11 +01:00
Przemyslaw Klys 26ecff620c fix typo 2025-08-16 08:15:09 +02:00
Przemysław Kłys 4358edfd8f Merge pull request #73 from ehmiiz/Invoke-GPOZaurrContent-docs-update
Update Invoke-GPOZaurrContent documentation and add new example
2025-02-19 17:38:38 +01:00
elarsson 31c7e9d817 Update Invoke-GPOZaurrContent documentation and add new example
- Enhance documentation for Invoke-GPOZaurrContent cmdlet with detailed parameter descriptions
- Add comprehensive parameter explanations and default values
- Include a new example demonstrating GPO retrieval by name with JSON conversion
- Update parameter set to include GPOName and GPOGUID parameters
2025-02-18 15:06:53 +01:00
Przemyslaw Klys 6718b35aaf Comment out self-import in module build configuration 2024-12-02 23:01:46 +01:00
Przemyslaw Klys 808af75ab9 Update module dependencies and version numbers 2024-12-02 22:57:14 +01:00
Przemyslaw Klys 6745e547a4 Update changelog 2024-12-02 22:54:24 +01:00
Przemyslaw Klys 642c4844a5 Add default file path handling for HTML report generation, fixes #58 2024-12-02 22:53:34 +01:00
14 changed files with 619 additions and 248 deletions
+3 -2
View File
@@ -27,7 +27,8 @@ Invoke-ModuleBuild -ModuleName 'GPOZaurr' {
}
New-ConfigurationManifest @Manifest
New-ConfigurationModule -Type RequiredModule -Name 'PSWriteColor', 'PSSharedGoods', 'ADEssentials', 'PSWriteHTML' -Guid Auto -Version Latest
New-ConfigurationModule -Type RequiredModule -Name 'PSWriteColor', 'PSSharedGoods', 'ADEssentials' -Guid Auto -Version Latest
New-ConfigurationModule -Type RequiredModule -Name "PSWriteHTML" -Guid Auto -Version "1.27.0"
#New-ConfigurationModule -Type ExternalModule -Name 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Management','Microsoft.PowerShell.Security'
New-ConfigurationModule -Type ApprovedModule -Name 'PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword', 'ADEssentials'
@@ -209,7 +210,7 @@ Invoke-ModuleBuild -ModuleName 'GPOZaurr' {
# configuration for documentation, at the same time it enables documentation processing
New-ConfigurationDocumentation -Enable:$false -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs'
New-ConfigurationImportModule -ImportSelf
#New-ConfigurationImportModule -ImportSelf
New-ConfigurationBuild -Enable:$true -SignModule -MergeModuleOnBuild -MergeFunctionsFromApprovedModules -CertificateThumbprint '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703'
+10
View File
@@ -1,5 +1,15 @@
# 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)
## 1.1.8 - 2024.11.20
### What's new
- Added `RestrictedGroups` to GroupAnalysis
+221 -166
View File
@@ -8,7 +8,7 @@ schema: 2.0.0
# Invoke-GPOZaurrContent
## SYNOPSIS
{{ Fill in the Synopsis }}
Invokes GPOZaurrContent function to retrieve Group Policy Objects information.
## SYNTAX
@@ -17,92 +17,45 @@ schema: 2.0.0
Invoke-GPOZaurrContent [-Forest <String>] [-ExcludeDomains <String[]>] [-IncludeDomains <String[]>]
[-ExtendedForestInformation <IDictionary>] [-Type <String[]>] [-Splitter <String>] [-FullObjects]
[-OutputType <String[]>] [-OutputPath <String>] [-Open] [-Online] [-CategoriesOnly] [-SingleObject]
[-SkipNormalize] [-SkipCleanup] [-Extended] [<CommonParameters>]
[-SkipNormalize] [-SkipCleanup] [-Extended] [-GPOName <String[]>] [-GPOGUID <String[]>]
[-ProgressAction <ActionPreference>] [<CommonParameters>]
```
### Local
```
Invoke-GPOZaurrContent [-GPOPath <String>] [-Type <String[]>] [-Splitter <String>] [-FullObjects]
[-OutputType <String[]>] [-OutputPath <String>] [-Open] [-Online] [-CategoriesOnly] [-SingleObject]
[-SkipNormalize] [-SkipCleanup] [-Extended] [<CommonParameters>]
[-SkipNormalize] [-SkipCleanup] [-Extended] [-ProgressAction <ActionPreference>] [<CommonParameters>]
```
## DESCRIPTION
{{ Fill in the Description }}
This function retrieves Group Policy Objects information based on the specified parameters.
It can search for GPOs in a forest, exclude specific domains, include specific domains, and provide extended forest information.
## EXAMPLES
### Example 1
```powershell
PS C:\> {{ Add example code here }}
### EXAMPLE 1
```
Invoke-GPOZaurrContent -Forest "Contoso" -IncludeDomains "Domain1", "Domain2" -Type "Security" -OutputType "HTML" -OutputPath "C:\Reports\GPOReport.html"
Retrieves security-related Group Policy Objects information for the specified domains and saves the output as an HTML file.
```
{{ Add example description here }}
### EXAMPLE 2
```
Invoke-GPOZaurrContent -GPOPath "CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=Contoso,DC=com" -Type "All" -OutputType "Object"
Retrieves all information for a specific Group Policy Object and outputs the result as an object.
```
### EXAMPLE 3
```
Invoke-GPOZaurrContent -GPOName "Group Policy Test" -SingleObject | ConvertTo-Json -Depth 3
Quickly view GPO settings by name in JSON format for easy inspection.
```
## PARAMETERS
### -CategoriesOnly
{{ Fill CategoriesOnly Description }}
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -ExcludeDomains
{{ Fill ExcludeDomains Description }}
```yaml
Type: String[]
Parameter Sets: Default
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -Extended
{{ Fill Extended Description }}
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -ExtendedForestInformation
{{ Fill ExtendedForestInformation Description }}
```yaml
Type: IDictionary
Parameter Sets: Default
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -Forest
{{ Fill Forest Description }}
Specifies the forest name to search for Group Policy Objects.
```yaml
Type: String
@@ -116,27 +69,12 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -FullObjects
{{ Fill FullObjects Description }}
### -ExcludeDomains
Specifies an array of domains to exclude from the search.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -GPOPath
{{ Fill GPOPath Description }}
```yaml
Type: String
Parameter Sets: Local
Type: String[]
Parameter Sets: Default
Aliases:
Required: False
@@ -147,7 +85,7 @@ Accept wildcard characters: False
```
### -IncludeDomains
{{ Fill IncludeDomains Description }}
Specifies an array of domains to include in the search.
```yaml
Type: String[]
@@ -161,12 +99,12 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -Online
{{ Fill Online Description }}
### -ExtendedForestInformation
Specifies additional information about the forest.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Type: IDictionary
Parameter Sets: Default
Aliases:
Required: False
@@ -176,27 +114,12 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -Open
{{ Fill Open Description }}
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -OutputPath
{{ Fill OutputPath Description }}
### -GPOPath
Specifies the path to a specific Group Policy Object.
```yaml
Type: String
Parameter Sets: (All)
Parameter Sets: Local
Aliases:
Required: False
@@ -206,59 +129,13 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -OutputType
{{ Fill OutputType Description }}
### -Type
Specifies the type of information to retrieve.
```yaml
Type: String[]
Parameter Sets: (All)
Aliases:
Accepted values: HTML, Object
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -SingleObject
{{ Fill SingleObject Description }}
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -SkipCleanup
{{ Fill SkipCleanup Description }}
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -SkipNormalize
{{ Fill SkipNormalize Description }}
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
@@ -268,7 +145,52 @@ Accept wildcard characters: False
```
### -Splitter
{{ Fill Splitter Description }}
Specifies the delimiter to use for splitting information.
```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: [System.Environment]::NewLine
Accept pipeline input: False
Accept wildcard characters: False
```
### -FullObjects
Indicates whether to retrieve full objects.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```
### -OutputType
Specifies the type of output (HTML or Object).
```yaml
Type: String[]
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: Object
Accept pipeline input: False
Accept wildcard characters: False
```
### -OutputPath
Specifies the path to save the output.
```yaml
Type: String
@@ -282,12 +204,133 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -Type
{{ Fill Type Description }}
### -Open
Indicates whether to open the output after retrieval.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```
### -Online
Indicates whether to retrieve information online.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```
### -CategoriesOnly
Indicates whether to retrieve only categories.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```
### -SingleObject
When enabled, returns a single consolidated object per GPO rather than multiple objects for each setting. For example, with drive mappings, instead of returning separate objects for each mapped drive in a GPO, it will return one object containing all drive mappings for that GPO. This affects how the data is structured in the output reports.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```
### -SkipNormalize
Indicates whether to skip normalization.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```
### -SkipCleanup
Indicates whether to skip cleanup of the output hashtable. When enabled, empty values in the output hashtable are preserved rather than being removed via Remove-EmptyValue. This parameter affects the final processing step before returning GPO analysis results.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```
### -Extended
When enabled, returns the complete output hashtable containing all GPO analysis data, including both the processed reports and raw categories. By default, only the processed reports are returned. This parameter is useful for debugging or when you need access to the underlying data structure.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```
### -GPOName
Specifies the name of the Group Policy Object to retrieve. The alias of this parameter is `Name`.
```yaml
Type: String[]
Parameter Sets: (All)
Parameter Sets: Default
Aliases: Name
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -GPOGUID
Specifies one or more Group Policy Object GUIDs to retrieve. This parameter allows you to target specific GPOs by their unique identifier rather than display name.
```yaml
Type: String[]
Parameter Sets: Default
Aliases:
Required: False
@@ -297,16 +340,28 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -ProgressAction
Specifies the action to take when progress is reported.
```yaml
Type: ActionPreference
Parameter Sets: (All)
Aliases: proga
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
## INPUTS
### None
## OUTPUTS
### System.Object
## NOTES
## RELATED LINKS
@@ -4,7 +4,7 @@
#$Objects = Get-GPOZaurrInheritance
#$Objects | Format-Table
# Get same output DN, CanonicalName, BlockInheritance (True/False) + Users/Computers + UsersCount/ComputerCount for those with Blocked Inhertiance
# Get same output DN, CanonicalName, BlockInheritance (True/False) + Users/Computers + UsersCount/ComputerCount for those with Blocked Inheritance
# This is so you can have a list what machines are affected
$ExcludedOU = @(
# Works on OU/
+6 -5
View File
@@ -4,31 +4,32 @@
CmdletsToExport = @()
CompanyName = 'Evotec'
CompatiblePSEditions = @('Desktop')
Copyright = '(c) 2011 - 2024 Przemyslaw Klys @ Evotec. All rights reserved.'
Copyright = '(c) 2011 - 2025 Przemyslaw Klys @ Evotec. All rights reserved.'
Description = 'Group Policy Eater is a PowerShell module that aims to gather information about Group Policies but also allows fixing issues that you may find in them.'
FunctionsToExport = @('Add-GPOPermission', 'Add-GPOZaurrPermission', 'Backup-GPOZaurr', 'Clear-GPOZaurrSysvolDFSR', 'ConvertFrom-CSExtension', 'Export-GPOZaurrContent', 'Find-CSExtension', 'Get-GPOZaurr', 'Get-GPOZaurrAD', 'Get-GPOZaurrBackupInformation', 'Get-GPOZaurrBroken', 'Get-GPOZaurrBrokenLink', 'Get-GPOZaurrDictionary', 'Get-GPOZaurrDuplicateObject', 'Get-GPOZaurrFiles', 'Get-GPOZaurrFilesPolicyDefinition', 'Get-GPOZaurrFolders', 'Get-GPOZaurrInheritance', 'Get-GPOZaurrLegacyFiles', 'Get-GPOZaurrLink', 'Get-GPOZaurrLinkSummary', 'Get-GPOZaurrMissingFiles', 'Get-GPOZaurrNetLogon', 'Get-GPOZaurrOrganizationalUnit', 'Get-GPOZaurrOwner', 'Get-GPOZaurrPassword', 'Get-GPOZaurrPermission', 'Get-GPOZaurrPermissionAnalysis', 'Get-GPOZaurrPermissionConsistency', 'Get-GPOZaurrPermissionIssue', 'Get-GPOZaurrPermissionRoot', 'Get-GPOZaurrPermissionSummary', 'Get-GPOZaurrRedirect', 'Get-GPOZaurrSysvolDFSR', 'Get-GPOZaurrUpdates', 'Get-GPOZaurrWMI', 'Invoke-GPOZaurr', 'Invoke-GPOZaurrContent', 'Invoke-GPOZaurrPermission', 'Invoke-GPOZaurrSupport', 'New-GPOZaurrWMI', 'Optimize-GPOZaurr', 'Remove-GPOPermission', 'Remove-GPOZaurr', 'Remove-GPOZaurrBroken', 'Remove-GPOZaurrDuplicateObject', 'Remove-GPOZaurrFolders', 'Remove-GPOZaurrLegacyFiles', 'Remove-GPOZaurrLinkEmptyOU', 'Remove-GPOZaurrPermission', 'Remove-GPOZaurrWMI', 'Repair-GPOZaurrBrokenLink', 'Repair-GPOZaurrNetLogonOwner', 'Repair-GPOZaurrPermission', 'Repair-GPOZaurrPermissionConsistency', 'Restore-GPOZaurr', 'Save-GPOZaurrFiles', 'Set-GPOOwner', 'Set-GPOZaurrOwner', 'Set-GPOZaurrStatus', 'Skip-GroupPolicy')
GUID = 'f7d4c9e4-0298-4f51-ad77-e8e3febebbde'
ModuleVersion = '1.1.8'
ModuleVersion = '1.1.10'
PowerShellVersion = '5.1'
PrivateData = @{
PSData = @{
ExternalModuleDependencies = @('CimCmdlets', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Security')
ProjectUri = 'https://github.com/EvotecIT/GPOZaurr'
RequireLicenseAcceptance = $false
Tags = @('Windows', 'ActiveDirectory', 'GPO', 'GroupPolicy')
}
}
RequiredModules = @(@{
Guid = '0b0ba5c5-ec85-4c2b-a718-874e55a8bc3f'
ModuleName = 'PSWriteColor'
ModuleVersion = '1.0.1'
ModuleVersion = '1.0.3'
}, @{
Guid = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe'
ModuleName = 'PSSharedGoods'
ModuleVersion = '0.0.300'
ModuleVersion = '0.0.312'
}, @{
Guid = '9fc9fd61-7f11-4f4b-a527-084086f1905f'
ModuleName = 'ADEssentials'
ModuleVersion = '0.0.226'
ModuleVersion = '0.0.267'
}, @{
Guid = 'a7bdf640-f5cb-4acf-9de0-365b322d245c'
ModuleName = 'PSWriteHTML'
+4 -4
View File
@@ -13,7 +13,7 @@ if ($AssemblyFolders.BaseName -contains 'Standard') {
}
}
$FoundErrors = @(
Foreach ($Import in @($Assembly)) {
foreach ($Import in @($Assembly)) {
try {
Add-Type -Path $Import.Fullname -ErrorAction Stop
} catch [System.Reflection.ReflectionTypeLoadException] {
@@ -35,10 +35,10 @@ $FoundErrors = @(
}
}
#Dot source the files
Foreach ($Import in @($Private + $Public)) {
Try {
foreach ($Import in @($Private + $Public)) {
try {
. $Import.Fullname
} Catch {
} catch {
Write-Error -Message "Failed to import functions from $($import.Fullname): $_"
$true
}
+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 @@
#}
}
}
}
}
@@ -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
}
}
+54 -54
View File
@@ -354,58 +354,58 @@
}
} else {
$GPOOutput = [PsCustomObject] @{
'DisplayName' = $XMLContent.GPO.Name
'DomainName' = $XMLContent.GPO.Identifier.Domain.'#text'
'GUID' = $XMLContent.GPO.Identifier.Identifier.InnerText -replace '{' -replace '}'
'Days' = (New-TimeSpan -Start ([DateTime] $XMLContent.GPO.ModifiedTime) -End (Get-Date)).Days
'Empty' = $Empty
'Linked' = $Linked
'Enabled' = $EnabledBool
'Optimized' = $Optimized
'Problem' = $Problem
'ApplyPermission' = $null
'Exclude' = $Exclude
'SizeMB' = [Math]::Round($TotalSize / 1MB, 2)
'Size' = $TotalSize
'Description' = $GPO.Description
'ComputerPolicies' = $XMLContent.GPO.Computer.ExtensionData.Name -join ", "
'UserPolicies' = $XMLContent.GPO.User.ExtensionData.Name -join ", "
'FilesCount' = $FilesCount
'LinksCount' = $LinksTotalCount
'LinksEnabledCount' = $LinksEnabledCount
'LinksDisabledCount' = $LinksDisabledCount
'EnabledDetails' = $Enabled
'ComputerProblem' = $ComputerProblem
'ComputerOptimized' = $ComputerOptimized
'UserProblem' = $UserProblem
'UserOptimized' = $UserOptimized
'ComputerSettingsAvailable' = $ComputerSettingsAvailable
'UserSettingsAvailable' = $UserSettingsAvailable
'DisplayName' = $XMLContent.GPO.Name
'DomainName' = $XMLContent.GPO.Identifier.Domain.'#text'
'GUID' = $XMLContent.GPO.Identifier.Identifier.InnerText -replace '{' -replace '}'
'Days' = (New-TimeSpan -Start ([DateTime] $XMLContent.GPO.ModifiedTime) -End (Get-Date)).Days
'Empty' = $Empty
'Linked' = $Linked
'Enabled' = $EnabledBool
'Optimized' = $Optimized
'Problem' = $Problem
'ApplyPermission' = $null
'Exclude' = $Exclude
'SizeMB' = [Math]::Round($TotalSize / 1MB, 2)
'Size' = $TotalSize
'Description' = $GPO.Description
'ComputerPolicies' = $XMLContent.GPO.Computer.ExtensionData.Name -join ", "
'UserPolicies' = $XMLContent.GPO.User.ExtensionData.Name -join ", "
'FilesCount' = $FilesCount
'LinksCount' = $LinksTotalCount
'LinksEnabledCount' = $LinksEnabledCount
'LinksDisabledCount' = $LinksDisabledCount
'EnabledDetails' = $Enabled
'ComputerProblem' = $ComputerProblem
'ComputerOptimized' = $ComputerOptimized
'UserProblem' = $UserProblem
'UserOptimized' = $UserOptimized
'ComputerSettingsAvailable' = $ComputerSettingsAvailable
'UserSettingsAvailable' = $UserSettingsAvailable
#'ComputerSettingsAvailableReal' = $ComputerSettingsAvailableReal
#'UserSettingsAvailableReal' = $UserSettingsAvailableReal
'ComputerSettingsTypes' = $OutputComputer.Name -join ", "
'UserSettingsTypes' = $OutputUser.Name -join ", "
'ComputerEnabled' = $ComputerEnabled
'UserEnabled' = $UserEnabled
'ComputerSettingsStatus' = if ($XMLContent.GPO.Computer.VersionDirectory -eq 0 -and $XMLContent.GPO.Computer.VersionSysvol -eq 0) { "NeverModified" } else { "Modified" }
'ComputerSetttingsVersionIdentical' = if ($XMLContent.GPO.Computer.VersionDirectory -eq $XMLContent.GPO.Computer.VersionSysvol) { $true } else { $false }
'ComputerSettings' = $XMLContent.GPO.Computer.ExtensionData.Extension
'UserSettingsStatus' = if ($XMLContent.GPO.User.VersionDirectory -eq 0 -and $XMLContent.GPO.User.VersionSysvol -eq 0) { "NeverModified" } else { "Modified" }
'UserSettingsVersionIdentical' = if ($XMLContent.GPO.User.VersionDirectory -eq $XMLContent.GPO.User.VersionSysvol) { $true } else { $false }
'UserSettings' = $XMLContent.GPO.User.ExtensionData.Extension
'NoSettings' = $NoSettings
'CreationTime' = [DateTime] $XMLContent.GPO.CreatedTime
'ModificationTime' = [DateTime] $XMLContent.GPO.ModifiedTime
'ReadTime' = [DateTime] $XMLContent.GPO.ReadTime
'WMIFilter' = $GPO.WmiFilter.name
'WMIFilterDescription' = $GPO.WmiFilter.Description
'GPODistinguishedName' = $GPO.Path
'GPOSysvolPath' = $SysvolGpoPath
'SDDL' = if ($Splitter -ne '') { $XMLContent.GPO.SecurityDescriptor.SDDL.'#text' -join $Splitter } else { $XMLContent.GPO.SecurityDescriptor.SDDL.'#text' }
'Owner' = $XMLContent.GPO.SecurityDescriptor.Owner.Name.'#text'
'OwnerSID' = $XMLContent.GPO.SecurityDescriptor.Owner.SID.'#text'
'OwnerType' = $OwnerType
'ACL' = @(
'ComputerSettingsTypes' = $OutputComputer.Name -join ", "
'UserSettingsTypes' = $OutputUser.Name -join ", "
'ComputerEnabled' = $ComputerEnabled
'UserEnabled' = $UserEnabled
'ComputerSettingsStatus' = if ($XMLContent.GPO.Computer.VersionDirectory -eq 0 -and $XMLContent.GPO.Computer.VersionSysvol -eq 0) { "NeverModified" } else { "Modified" }
'ComputerSettingsVersionIdentical' = if ($XMLContent.GPO.Computer.VersionDirectory -eq $XMLContent.GPO.Computer.VersionSysvol) { $true } else { $false }
'ComputerSettings' = $XMLContent.GPO.Computer.ExtensionData.Extension
'UserSettingsStatus' = if ($XMLContent.GPO.User.VersionDirectory -eq 0 -and $XMLContent.GPO.User.VersionSysvol -eq 0) { "NeverModified" } else { "Modified" }
'UserSettingsVersionIdentical' = if ($XMLContent.GPO.User.VersionDirectory -eq $XMLContent.GPO.User.VersionSysvol) { $true } else { $false }
'UserSettings' = $XMLContent.GPO.User.ExtensionData.Extension
'NoSettings' = $NoSettings
'CreationTime' = [DateTime] $XMLContent.GPO.CreatedTime
'ModificationTime' = [DateTime] $XMLContent.GPO.ModifiedTime
'ReadTime' = [DateTime] $XMLContent.GPO.ReadTime
'WMIFilter' = $GPO.WmiFilter.name
'WMIFilterDescription' = $GPO.WmiFilter.Description
'GPODistinguishedName' = $GPO.Path
'GPOSysvolPath' = $SysvolGpoPath
'SDDL' = if ($Splitter -ne '') { $XMLContent.GPO.SecurityDescriptor.SDDL.'#text' -join $Splitter } else { $XMLContent.GPO.SecurityDescriptor.SDDL.'#text' }
'Owner' = $XMLContent.GPO.SecurityDescriptor.Owner.Name.'#text'
'OwnerSID' = $XMLContent.GPO.SecurityDescriptor.Owner.SID.'#text'
'OwnerType' = $OwnerType
'ACL' = @(
[PsCustomObject] @{
'Name' = $XMLContent.GPO.SecurityDescriptor.Owner.Name.'#text'
'Sid' = $XMLContent.GPO.SecurityDescriptor.Owner.SID.'#text'
@@ -425,10 +425,10 @@
}
}
)
'Auditing' = if ($XMLContent.GPO.SecurityDescriptor.AuditingPresent.'#text' -eq 'true') { $true } else { $false }
'Links' = $Links
'LinksObjects' = $LinksObjects
'GPOObject' = $GPO
'Auditing' = if ($XMLContent.GPO.SecurityDescriptor.AuditingPresent.'#text' -eq 'true') { $true } else { $false }
'Links' = $Links
'LinksObjects' = $LinksObjects
'GPOObject' = $GPO
}
if ($GPOOutput.ACL) {
$GPOOutput.ApplyPermission = $false
@@ -1,5 +1,5 @@
$GPOZaurrBlockedInheritance = [ordered] @{
Name = 'Group Policy Blocked Inhertiance'
Name = 'Group Policy Blocked Inheritance'
Enabled = $true
ActionRequired = $null
Data = $null
+23 -7
View File
@@ -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
}
}
}
}
+4 -4
View File
@@ -126,6 +126,10 @@
}
}
if (-not $FilePath) {
$FilePath = Get-FileName -Extension 'html' -Temporary -Name "GpoZaurr"
}
# Build data
foreach ($T in $Script:GPOConfiguration.Keys) {
if ($Script:GPOConfiguration[$T].Enabled -eq $true) {
@@ -201,10 +205,6 @@
if (-not $SplitReports) {
# Generate pretty HTML
$TimeLogHTML = Start-TimeLog
if (-not $FilePath) {
$FilePath = Get-FileName -Extension 'html' -Temporary
}
New-HTMLReportAll -FilePath $FilePath -Online:$Online -HideHTML:$HideHTML -Type $Type
$TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner
+38 -1
View File
@@ -59,6 +59,10 @@
Invoke-GPOZaurrContent -GPOPath "CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=Contoso,DC=com" -Type "All" -OutputType "Object"
Retrieves all information for a specific Group Policy Object and outputs the result as an object.
.EXAMPLE
Invoke-GPOZaurrContent -GPOName "Group Policy Test" -SingleObject | ConvertTo-Json -Depth 3
Quickly view GPO settings by name in JSON format for easy inspection.
#>
[alias('Find-GPO')]
[cmdletBinding(DefaultParameterSetName = 'Default')]
@@ -283,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
@@ -348,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
+134
View File
@@ -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'
}
}
}