diff --git a/Add-Named-Instances/Add-Named-Instances.ps1 b/Add-Named-Instances/Add-Named-Instances.ps1 index 2bbce9e..4785665 100644 --- a/Add-Named-Instances/Add-Named-Instances.ps1 +++ b/Add-Named-Instances/Add-Named-Instances.ps1 @@ -64,38 +64,6 @@ script now uses shared Write-ScriptLog and Remove-OldLog. Log filename format changed from Add-Named-Instances_yyyyMMdd_HHmmss.log to yyyyMMdd_HHmmss_Add-Named-Instances.log. - 1.0.0 - 2026-05-13 - Initial release. - 1.1.0 - 2026-05-13 - Added explicit param() block; moved group name constants to - dedicated Configuration region; added early exit when no instances - are found; replaced exit 1 with throw; passed MemberCache - explicitly to Add-AccountToGroup; added pipeline result - collection and verbose summary. - 1.2.0 - 2026-05-19 - Added Write-Log helper; log file written to C:\Temp with - timestamp in filename; all verbose/warning output now also - persisted to the log file. - 1.3.0 - 2026-05-19 - Fixed WhatIf bug: Add-AccountToGroup now emits a WouldAdd - result object instead of $null when ShouldProcess returns - $false, preventing null entries in the results list. - Added Write-Log calls for successful Added and AlreadyMember - actions. Updated summary to report WouldAdd count. - Expanded .DESCRIPTION and .OUTPUTS documentation. - 1.4.0 - 2026-05-19 - Added Remove-OldLogs helper; log files older than 30 days - are deleted from C:\Temp at the end of each run. - 1.5.0 - 2026-05-19 - Added [Parameter()] and [ValidateNotNullOrEmpty()] to all - function parameters per workspace coding guidelines. - Removed Write-Error from Write-Log ERROR level: it created a - duplicate error record in $Error alongside the throw that always - follows; the log file entry is sufficient. - Fixed early-return path: log cleanup now runs before the return - when no instances are discovered, so old log files are still - pruned on those runs. - 1.6.0 - 2026-05-19 - GPO startup script compatibility: replaced em dash characters - in Write-Log string literals with ' - ' to prevent a PS 5.1 - cp1252 parse error (UTF-8 E2 80 94 decodes to curly-quote in - cp1252, terminating the string unexpectedly). Added - $ErrorActionPreference = 'Stop' so unexpected errors outside - try/catch are caught rather than silently continuing in a - non-interactive SYSTEM session. 1.9.0 - 2026-05-21 - Removed unused State property from $ServiceAccounts query; switched to Select-Object -ExpandProperty StartName for a cleaner string list (improvement 6). Wrapped Get-Service and @@ -123,6 +91,38 @@ groups" to "five groups"; fixed misleading inline comment on $LogonAsServiceGroup; added missing #endregion for #region Add members to groups. + 1.6.0 - 2026-05-19 - GPO startup script compatibility: replaced em dash characters + in Write-Log string literals with ' - ' to prevent a PS 5.1 + cp1252 parse error (UTF-8 E2 80 94 decodes to curly-quote in + cp1252, terminating the string unexpectedly). Added + $ErrorActionPreference = 'Stop' so unexpected errors outside + try/catch are caught rather than silently continuing in a + non-interactive SYSTEM session. + 1.5.0 - 2026-05-19 - Added [Parameter()] and [ValidateNotNullOrEmpty()] to all + function parameters per workspace coding guidelines. + Removed Write-Error from Write-Log ERROR level: it created a + duplicate error record in $Error alongside the throw that always + follows; the log file entry is sufficient. + Fixed early-return path: log cleanup now runs before the return + when no instances are discovered, so old log files are still + pruned on those runs. + 1.4.0 - 2026-05-19 - Added Remove-OldLogs helper; log files older than 30 days + are deleted from C:\Temp at the end of each run. + 1.3.0 - 2026-05-19 - Fixed WhatIf bug: Add-AccountToGroup now emits a WouldAdd + result object instead of $null when ShouldProcess returns + $false, preventing null entries in the results list. + Added Write-Log calls for successful Added and AlreadyMember + actions. Updated summary to report WouldAdd count. + Expanded .DESCRIPTION and .OUTPUTS documentation. + 1.2.0 - 2026-05-19 - Added Write-Log helper; log file written to C:\Temp with + timestamp in filename; all verbose/warning output now also + persisted to the log file. + 1.1.0 - 2026-05-13 - Added explicit param() block; moved group name constants to + dedicated Configuration region; added early exit when no instances + are found; replaced exit 1 with throw; passed MemberCache + explicitly to Add-AccountToGroup; added pipeline result + collection and verbose summary. + 1.0.0 - 2026-05-13 - Initial release. #> [CmdletBinding(SupportsShouldProcess)] param( diff --git a/CleanupPolicies/Cleanup-Policies.md b/CleanupPolicies/Cleanup-Policies.md index 5c12f26..ae9cc80 100644 --- a/CleanupPolicies/Cleanup-Policies.md +++ b/CleanupPolicies/Cleanup-Policies.md @@ -140,6 +140,7 @@ flowchart TD | Version | Date | Author | Notes | | --- | --- | --- | --- | +| 1.4.0 | 2026-09-09 | M. Stam | Moved module-check loop to shared `Import-RequiredModule`. Fixed latent bug: the optional `ImportExcel` module was previously imported eagerly with `-ErrorAction Stop`, which would terminate the script when `ImportExcel` was not installed instead of falling back to CSV as documented; `ImportExcel` is no longer imported upfront (`Export-Excel` auto-loads it when available). | | 1.3.0 | 2026-07-13 | M. Stam | Migrated logging to shared module `Shared\PublicScripts.psm1`. Replaced `Tee-Object` log writer (produced UTF-8 BOM) with `Write-ScriptLog` (UTF-8 without BOM). | | 1.2.0 | 2026-06-25 | M. Stam | Removed dead `-not $_.Denied` filter (`GPPermission` has no `Denied` property; `GpoApply` already implies an Allow ACE); updated How-it-works description | | 1.1.0 | 2026-06-25 | M. Stam | Fixed Apply ACE detection to use `Get-GPPermission` (locale-independent); added per-GPO `[HasApplyACE]`/`[NoApplyACE]` log entries | diff --git a/CleanupPolicies/Cleanup-Policies.ps1 b/CleanupPolicies/Cleanup-Policies.ps1 index da9e1b6..290b1ba 100644 --- a/CleanupPolicies/Cleanup-Policies.ps1 +++ b/CleanupPolicies/Cleanup-Policies.ps1 @@ -38,8 +38,8 @@ Runs against contoso.com and writes the report to C:\Reports. .NOTES Author : M. Stam - Date : 2026-07-13 - Version : 1.3.0 + Date : 2026-09-09 + Version : 1.4.0 Requires : GroupPolicy module (RSAT-GPMC) ActiveDirectory module (RSAT-AD-PowerShell) @@ -55,6 +55,13 @@ Group Policy' (Dutch: 'Groepsbeleid toepassen'), which caused false positives. Version history: + 1.4.0 2026-09-09 M. Stam Moved Import-Module module-check loop to shared + Import-RequiredModule. Fixed latent bug: the optional + ImportExcel module was previously imported eagerly with + -ErrorAction Stop, which would terminate the script when + ImportExcel was not installed instead of falling back to + CSV as documented; ImportExcel is no longer imported + upfront (Export-Excel auto-loads it when available). 1.3.0 2026-07-13 M. Stam Migrated logging to shared module Shared\PublicScripts.psm1. Replaced Tee-Object log writer (produced UTF-8 BOM) with Write-ScriptLog (UTF-8 without BOM). @@ -138,18 +145,7 @@ Write-ScriptLog "Starting GPO inventory for domain: $Domain" #Import modules Write-ScriptLog "Checking required modules..." -if (-not (Get-Module -Name GroupPolicy)) { - Write-ScriptLog "Importing GroupPolicy module..." - Import-Module GroupPolicy -ErrorAction Stop -} -if (-not (Get-Module -Name ImportExcel)) { - Write-ScriptLog "Importing ImportExcel module..." - Import-Module ImportExcel -ErrorAction Stop -} -if (-not (Get-Module -Name ActiveDirectory)) { - Write-ScriptLog "Importing ActiveDirectory module..." - Import-Module ActiveDirectory -ErrorAction Stop -} +Import-RequiredModule -Name 'GroupPolicy', 'ActiveDirectory' # Collect all GPO links Write-ScriptLog "Collecting all GPO links..." diff --git a/Compare-GPOsByOU/Compare-GPOsByOU.md b/Compare-GPOsByOU/Compare-GPOsByOU.md index 82a09da..f7f3175 100644 --- a/Compare-GPOsByOU/Compare-GPOsByOU.md +++ b/Compare-GPOsByOU/Compare-GPOsByOU.md @@ -304,6 +304,7 @@ flowchart TD | Version | Date | Author | Changes | | --- | --- | --- | --- | +| 1.6.0 | 2026-09-09 | M. Stam | Moved `Get-XmlInnerText` and `Get-GpoSetting` to shared module `Shared\PublicScripts.psm1` (removed duplication with `Get-AllGPOSettings.ps1`). Moved module-check loop to shared `Import-RequiredModule`. | | 1.5.0 | 2026-07-13 | M. Stam | Migrated logging to shared module `Shared\PublicScripts.psm1`. Removed local `Write-ScriptLog` implementation and `$script:LogEncoding` variable; these are now provided by the module. | | 1.4.0 | 2026-07-13 | M. Stam | Parallel `Get-GPInheritance` on PS 7+; elapsed-time logging per phase; UTF-8 NoBOM log writer; `Dictionary` + `TryGetValue`; `HashSet` for orphaned lookup; `GpoStatus` cached per link; `SearchBase` validation merged into enumeration; removed redundant `-Properties 'Name'` | | 1.3.0 | 2026-07-10 | M. Stam | Added `-DomainController`, `-PassThru`; `Write-Progress`; orphaned GPO detection; Security Options, Restricted Groups, System Services, Windows Firewall rules in `-CompareSettings`; improved conflict detection (respects Computer/User area); parallel report fetching on PS 7+; WMI filter null guard; fixed double-logging; `OutputPath` auto-create; sort before export | diff --git a/Compare-GPOsByOU/Compare-GPOsByOU.ps1 b/Compare-GPOsByOU/Compare-GPOsByOU.ps1 index 01346d8..6590e3e 100644 --- a/Compare-GPOsByOU/Compare-GPOsByOU.ps1 +++ b/Compare-GPOsByOU/Compare-GPOsByOU.ps1 @@ -90,8 +90,8 @@ Returns the result data to the pipeline for further filtering. .NOTES Author : M. Stam - Date : 2026-07-13 - Version : 1.5.0 + Date : 2026-09-09 + Version : 1.6.0 Requires : GroupPolicy module (RSAT-GPMC) ActiveDirectory module (RSAT-AD-PowerShell) @@ -110,6 +110,10 @@ root\CIMv2;SELECT * FROM Win32_OperatingSystem WHERE Version LIKE "10.%" Version history: + 1.6.0 2026-09-09 M. Stam Moved Get-XmlInnerText and Get-GpoSetting to shared module + Shared\PublicScripts.psm1 (removed duplication with + Get-AllGPOSettings.ps1). Moved module-check loop to shared + Import-RequiredModule. 1.5.0 2026-07-13 M. Stam Migrated logging to shared module Shared\PublicScripts.psm1. Removed local Write-ScriptLog implementation and $script:LogEncoding variable; these are now provided by @@ -179,182 +183,6 @@ $null = Initialize-ScriptLog -LogDirectory (Join-Path $script:ScriptRoot 'Log') -ScriptName 'Compare-GPOsByOU' -Tag $Domain #endregion -#region Helper functions -function Get-XmlInnerText { - <# - .SYNOPSIS Returns InnerText of a child XML node, or $null when absent. - #> - param ([System.Xml.XmlNode] $ParentNode, [string] $LocalName) - $child = $ParentNode.SelectSingleNode("*[local-name()='$LocalName']") - if ($null -ne $child) { return $child.InnerText } - return $null -} - -function Get-GpoSetting { - <# - .SYNOPSIS Parses a GPO XML report and returns a flat list of settings. - .NOTES Uses Write-Warning (not Write-ScriptLog) so it is safe to call from - ForEach-Object -Parallel runspaces that cannot write to the shared log file. - #> - param ( - [string] $GpoGuid, - [string] $GpoName, - [string] $DomainFqdn, - [string] $Server - ) - - $results = [System.Collections.Generic.List[PSCustomObject]]::new() - - try { - $reportParams = @{ - Guid = $GpoGuid - ReportType = 'Xml' - Domain = $DomainFqdn - ErrorAction = 'Stop' - } - if (-not [string]::IsNullOrEmpty($Server)) { $reportParams['Server'] = $Server } - [xml] $xml = Get-GPOReport @reportParams - } - catch { - Write-Warning "Cannot retrieve report for '$GpoName': $_" - return $results - } - - foreach ($area in @('Computer', 'User')) { - $areaNode = $xml.SelectSingleNode("//*[local-name()='$area']") - if ($null -eq $areaNode) { continue } - - # Administrative Templates - foreach ($policy in @($areaNode.SelectNodes(".//*[local-name()='Policy']"))) { - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Administrative Templates' - Category = Get-XmlInnerText $policy 'Category' - SettingName = Get-XmlInnerText $policy 'Name' - SettingState = Get-XmlInnerText $policy 'State' - SettingValue = $null - }) - } - - # Security Settings – Account Policies (password, lockout, Kerberos) - foreach ($account in @($areaNode.SelectNodes(".//*[local-name()='Account']"))) { - $value = Get-XmlInnerText $account 'SettingNumber' - if ($null -eq $value) { $value = Get-XmlInnerText $account 'SettingBoolean' } - if ($null -eq $value) { $value = Get-XmlInnerText $account 'SettingString' } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'Account Policies' - SettingName = Get-XmlInnerText $account 'Name' - SettingState = $null - SettingValue = $value - }) - } - - # Security Settings – User Rights Assignment - foreach ($ura in @($areaNode.SelectNodes(".//*[local-name()='UserRightsAssignment']"))) { - $members = @($ura.SelectNodes("*[local-name()='Member']/*[local-name()='Name']")) | - ForEach-Object { $_.InnerText } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'User Rights Assignment' - SettingName = Get-XmlInnerText $ura 'Name' - SettingState = $null - SettingValue = ($members -join '; ') - }) - } - - # Security Settings – Audit Policy - foreach ($audit in @($areaNode.SelectNodes(".//*[local-name()='AuditSetting']"))) { - $name = Get-XmlInnerText $audit 'SubcategoryName' - if ($null -eq $name) { $name = Get-XmlInnerText $audit 'Category' } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'Audit Policy' - SettingName = $name - SettingState = $null - SettingValue = Get-XmlInnerText $audit 'SettingValue' - }) - } - - # Security Settings – Security Options (registry-based policy settings) - foreach ($secOpt in @($areaNode.SelectNodes(".//*[local-name()='SecurityOptions']"))) { - $displayNode = $secOpt.SelectSingleNode( - "*[local-name()='Display']/*[local-name()='Name']") - $displayName = if ($null -ne $displayNode) { $displayNode.InnerText } ` - else { Get-XmlInnerText $secOpt 'KeyName' } - $value = Get-XmlInnerText $secOpt 'SettingNumber' - if ($null -eq $value) { $value = Get-XmlInnerText $secOpt 'SettingString' } - if ($null -eq $value) { $value = Get-XmlInnerText $secOpt 'SettingBoolean' } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'Security Options' - SettingName = $displayName - SettingState = $null - SettingValue = $value - }) - } - - # Security Settings – Restricted Groups - foreach ($rg in @($areaNode.SelectNodes(".//*[local-name()='RestrictedGroup']"))) { - $members = @($rg.SelectNodes(".//*[local-name()='Member']/*[local-name()='Name']")) | - ForEach-Object { $_.InnerText } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'Restricted Groups' - SettingName = Get-XmlInnerText $rg 'GroupName' - SettingState = $null - SettingValue = ($members -join '; ') - }) - } - - # Security Settings – System Services - foreach ($svc in @($areaNode.SelectNodes(".//*[local-name()='NTService']"))) { - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'System Services' - SettingName = Get-XmlInnerText $svc 'ServiceName' - SettingState = Get-XmlInnerText $svc 'StartupMode' - SettingValue = $null - }) - } - - # Scripts (Startup / Shutdown / Logon / Logoff) - foreach ($scriptItem in @($areaNode.SelectNodes(".//*[local-name()='Script']"))) { - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Scripts' - Category = Get-XmlInnerText $scriptItem 'Type' - SettingName = Get-XmlInnerText $scriptItem 'CmdLine' - SettingState = 'Configured' - SettingValue = Get-XmlInnerText $scriptItem 'Parameters' - }) - } - - # Windows Firewall Rules (Windows Defender Firewall with Advanced Security) - foreach ($fwSection in @($areaNode.SelectNodes(".//*[local-name()='FirewallRules']"))) { - foreach ($rule in @($fwSection.SelectNodes("*[local-name()='Rule']"))) { - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Windows Firewall' - Category = Get-XmlInnerText $rule 'Profile' - SettingName = Get-XmlInnerText $rule 'Name' - SettingState = Get-XmlInnerText $rule 'Active' - SettingValue = Get-XmlInnerText $rule 'Action' - }) - } - } - } - - return $results -} -#endregion - #region Main # Resolve the server target used for all AD and GPO calls $adServer = if ($PSBoundParameters.ContainsKey('DomainController')) { $DomainController } else { $Domain } @@ -364,12 +192,7 @@ Write-ScriptLog "Starting GPO-by-OU comparison for domain: $Domain (server: $adS # Import required modules Write-ScriptLog 'Checking required modules...' -foreach ($mod in 'GroupPolicy', 'ActiveDirectory') { - if (-not (Get-Module -Name $mod)) { - Write-ScriptLog "Importing module: $mod" - Import-Module $mod -ErrorAction Stop - } -} +Import-RequiredModule -Name 'GroupPolicy', 'ActiveDirectory' # Pre-load all GPOs — Dictionary enables single-lookup TryGetValue Write-ScriptLog 'Retrieving all GPOs...' diff --git a/Generate-RDCManConfigs/Generate-RDCManConfigs.md b/Generate-RDCManConfigs/Generate-RDCManConfigs.md index b540f35..1780cea 100644 --- a/Generate-RDCManConfigs/Generate-RDCManConfigs.md +++ b/Generate-RDCManConfigs/Generate-RDCManConfigs.md @@ -180,6 +180,6 @@ RDCMan | Version | Date | Author | Change | | ------- | ---- | ------ | ------ | -| 1.0.0 | — | Joey Eckelbarger | Initial release | -| 2.0.0 | 2026-06-17 | Marcel Stam | Added `param` block with `ValidateScript`; fixed `-Credential` not being passed to `Get-ADComputer`; added `-UseCurrentCredentials`, `-Force`, `SupportsShouldProcess`/`-WhatIf`; `try/catch` on all external calls; `Write-ScriptLog` to `.\Log\`; XPath injection protection; replaced shared mutable XML templates with fresh-node helpers; merged OU-tagging loop into XML-building loop; `Join-Path` for all paths; removed `Select` alias and dead variables | | 2.1.0 | 2026-07-13 | Marcel Stam | Migrated logging to shared module `Shared\PublicScripts.psm1`. Log filename now includes the script name: `yyyyMMdd_HHmmss_Generate-RDCManConfigs.log`. | +| 2.0.0 | 2026-06-17 | Marcel Stam | Added `param` block with `ValidateScript`; fixed `-Credential` not being passed to `Get-ADComputer`; added `-UseCurrentCredentials`, `-Force`, `SupportsShouldProcess`/`-WhatIf`; `try/catch` on all external calls; `Write-ScriptLog` to `.\Log\`; XPath injection protection; replaced shared mutable XML templates with fresh-node helpers; merged OU-tagging loop into XML-building loop; `Join-Path` for all paths; removed `Select` alias and dead variables | +| 1.0.0 | — | Joey Eckelbarger | Initial release | diff --git a/Generate-RDCManConfigs/Generate-RDCManConfigs.ps1 b/Generate-RDCManConfigs/Generate-RDCManConfigs.ps1 index 662bf6f..431139e 100644 --- a/Generate-RDCManConfigs/Generate-RDCManConfigs.ps1 +++ b/Generate-RDCManConfigs/Generate-RDCManConfigs.ps1 @@ -52,14 +52,12 @@ Editor: Marcel Stam Version: 2.1.0 - Version History: - 1.0.0 - Joey Eckelbarger - Initial release - 2.1.0 - Marcel Stam - 2026-07-13 - Migrated logging to shared module Shared\PublicScripts.psm1. - Log filename now includes the script name: - yyyyMMdd_HHmmss_Generate-RDCManConfigs.log. - 2.0.0 - Marcel Stam - 2026-06-17 - Added param block with ValidateScript + Version history: + 2.1.0 - 2026-07-13 - Marcel Stam - Migrated logging to shared module + Shared\PublicScripts.psm1. Log filename now + includes the script name: + yyyyMMdd_HHmmss_Generate-RDCManConfigs.log. + 2.0.0 - 2026-06-17 - Marcel Stam - Added param block with ValidateScript Fixed: -Credential now passed to Get-ADComputer Added -UseCurrentCredentials switch for unattended use Added -Force switch to control overwrite behaviour @@ -72,6 +70,7 @@ Merged OU-tagging loop into XML-building loop Replaced string concatenation with Join-Path Removed Select alias; removed dead variables + 1.0.0 - — - Joey Eckelbarger - Initial release #> [CmdletBinding(SupportsShouldProcess)] diff --git a/Get-AllGPOSettings/Get-AllGPOSettings.md b/Get-AllGPOSettings/Get-AllGPOSettings.md index 4e0682f..be03a7a 100644 --- a/Get-AllGPOSettings/Get-AllGPOSettings.md +++ b/Get-AllGPOSettings/Get-AllGPOSettings.md @@ -118,5 +118,6 @@ $settings | Where-Object { $_.ExtensionType -eq 'Administrative Templates' } | Version | Date | Author | Changes | | --- | --- | --- | --- | +| 1.1.0 | 2026-09-09 | M. Stam | Moved `Get-XmlInnerText` and `Get-GpoSetting` to shared module `Shared\PublicScripts.psm1` (removed duplication with `Compare-GPOsByOU.ps1`). Moved module-check block to shared `Import-RequiredModule`. | | 1.0.1 | 2026-08-20 | M. Stam | Fixed a crash ("The property 'Name' cannot be found on this object") that occurred when a GPO referenced a WMI filter deleted from Active Directory; WMI filter name resolution now uses try/catch instead of a bare null-check. | | 1.0.0 | 2026-08-20 | M. Stam | Initial release. | diff --git a/Get-AllGPOSettings/Get-AllGPOSettings.ps1 b/Get-AllGPOSettings/Get-AllGPOSettings.ps1 index 0e2c417..e894b87 100644 --- a/Get-AllGPOSettings/Get-AllGPOSettings.ps1 +++ b/Get-AllGPOSettings/Get-AllGPOSettings.ps1 @@ -57,8 +57,8 @@ Returns the result data to the pipeline for further filtering. .NOTES Author : M. Stam - Date : 2026-08-20 - Version : 1.0.1 + Date : 2026-09-09 + Version : 1.1.0 Requires : GroupPolicy module (RSAT-GPMC) ImportExcel module (optional; https://github.com/dfinke/ImportExcel) @@ -84,6 +84,10 @@ this object." under Set-StrictMode -Version Latest. Version history: + 1.1.0 2026-09-09 M. Stam Moved Get-XmlInnerText and Get-GpoSetting to shared module + Shared\PublicScripts.psm1 (removed duplication with + Compare-GPOsByOU.ps1). Moved module-check block to shared + Import-RequiredModule. 1.0.1 2026-08-20 M. Stam Fixed crash ("The property 'Name' cannot be found on this object") when a GPO references a WMI filter that no longer exists in AD; @@ -129,16 +133,6 @@ $null = Initialize-ScriptLog -LogDirectory (Join-Path $script:ScriptRoot 'Log') #endregion #region Helper functions -function Get-XmlInnerText { - <# - .SYNOPSIS Returns InnerText of a child XML node, or $null when absent. - #> - param ([System.Xml.XmlNode] $ParentNode, [string] $LocalName) - $child = $ParentNode.SelectSingleNode("*[local-name()='$LocalName']") - if ($null -ne $child) { return $child.InnerText } - return $null -} - function Get-GpoWmiFilterName { <# .SYNOPSIS Safely resolves a GPO's WMI filter display name. @@ -157,170 +151,6 @@ function Get-GpoWmiFilterName { return $null } } - -function Get-GpoSetting { - <# - .SYNOPSIS Parses a GPO XML report and returns a flat list of settings. - .NOTES Uses Write-Warning (not Write-ScriptLog) so it is safe to call from - ForEach-Object -Parallel runspaces that cannot write to the shared log file. - #> - param ( - [string] $GpoGuid, - [string] $GpoName, - [string] $DomainFqdn, - [string] $Server - ) - - $results = [System.Collections.Generic.List[PSCustomObject]]::new() - - try { - $reportParams = @{ - Guid = $GpoGuid - ReportType = 'Xml' - Domain = $DomainFqdn - ErrorAction = 'Stop' - } - if (-not [string]::IsNullOrEmpty($Server)) { $reportParams['Server'] = $Server } - [xml] $xml = Get-GPOReport @reportParams - } - catch { - Write-Warning "Cannot retrieve report for '$GpoName': $_" - return $results - } - - foreach ($area in @('Computer', 'User')) { - $areaNode = $xml.SelectSingleNode("//*[local-name()='$area']") - if ($null -eq $areaNode) { continue } - - # Administrative Templates - foreach ($policy in @($areaNode.SelectNodes(".//*[local-name()='Policy']"))) { - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Administrative Templates' - Category = Get-XmlInnerText $policy 'Category' - SettingName = Get-XmlInnerText $policy 'Name' - SettingState = Get-XmlInnerText $policy 'State' - SettingValue = $null - }) - } - - # Security Settings – Account Policies (password, lockout, Kerberos) - foreach ($account in @($areaNode.SelectNodes(".//*[local-name()='Account']"))) { - $value = Get-XmlInnerText $account 'SettingNumber' - if ($null -eq $value) { $value = Get-XmlInnerText $account 'SettingBoolean' } - if ($null -eq $value) { $value = Get-XmlInnerText $account 'SettingString' } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'Account Policies' - SettingName = Get-XmlInnerText $account 'Name' - SettingState = $null - SettingValue = $value - }) - } - - # Security Settings – User Rights Assignment - foreach ($ura in @($areaNode.SelectNodes(".//*[local-name()='UserRightsAssignment']"))) { - $members = @($ura.SelectNodes("*[local-name()='Member']/*[local-name()='Name']")) | - ForEach-Object { $_.InnerText } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'User Rights Assignment' - SettingName = Get-XmlInnerText $ura 'Name' - SettingState = $null - SettingValue = ($members -join '; ') - }) - } - - # Security Settings – Audit Policy - foreach ($audit in @($areaNode.SelectNodes(".//*[local-name()='AuditSetting']"))) { - $name = Get-XmlInnerText $audit 'SubcategoryName' - if ($null -eq $name) { $name = Get-XmlInnerText $audit 'Category' } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'Audit Policy' - SettingName = $name - SettingState = $null - SettingValue = Get-XmlInnerText $audit 'SettingValue' - }) - } - - # Security Settings – Security Options (registry-based policy settings) - foreach ($secOpt in @($areaNode.SelectNodes(".//*[local-name()='SecurityOptions']"))) { - $displayNode = $secOpt.SelectSingleNode( - "*[local-name()='Display']/*[local-name()='Name']") - $displayName = if ($null -ne $displayNode) { $displayNode.InnerText } ` - else { Get-XmlInnerText $secOpt 'KeyName' } - $value = Get-XmlInnerText $secOpt 'SettingNumber' - if ($null -eq $value) { $value = Get-XmlInnerText $secOpt 'SettingString' } - if ($null -eq $value) { $value = Get-XmlInnerText $secOpt 'SettingBoolean' } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'Security Options' - SettingName = $displayName - SettingState = $null - SettingValue = $value - }) - } - - # Security Settings – Restricted Groups - foreach ($rg in @($areaNode.SelectNodes(".//*[local-name()='RestrictedGroup']"))) { - $members = @($rg.SelectNodes(".//*[local-name()='Member']/*[local-name()='Name']")) | - ForEach-Object { $_.InnerText } - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'Restricted Groups' - SettingName = Get-XmlInnerText $rg 'GroupName' - SettingState = $null - SettingValue = ($members -join '; ') - }) - } - - # Security Settings – System Services - foreach ($svc in @($areaNode.SelectNodes(".//*[local-name()='NTService']"))) { - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Security Settings' - Category = 'System Services' - SettingName = Get-XmlInnerText $svc 'ServiceName' - SettingState = Get-XmlInnerText $svc 'StartupMode' - SettingValue = $null - }) - } - - # Scripts (Startup / Shutdown / Logon / Logoff) - foreach ($scriptItem in @($areaNode.SelectNodes(".//*[local-name()='Script']"))) { - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Scripts' - Category = Get-XmlInnerText $scriptItem 'Type' - SettingName = Get-XmlInnerText $scriptItem 'CmdLine' - SettingState = 'Configured' - SettingValue = Get-XmlInnerText $scriptItem 'Parameters' - }) - } - - # Windows Firewall Rules (Windows Defender Firewall with Advanced Security) - foreach ($fwSection in @($areaNode.SelectNodes(".//*[local-name()='FirewallRules']"))) { - foreach ($rule in @($fwSection.SelectNodes("*[local-name()='Rule']"))) { - $results.Add([PSCustomObject]@{ - Area = $area - ExtensionType = 'Windows Firewall' - Category = Get-XmlInnerText $rule 'Profile' - SettingName = Get-XmlInnerText $rule 'Name' - SettingState = Get-XmlInnerText $rule 'Active' - SettingValue = Get-XmlInnerText $rule 'Action' - }) - } - } - } - - return $results -} #endregion #region Main @@ -332,10 +162,7 @@ Write-ScriptLog "Starting all-GPO settings report for domain: $Domain (server: $ # Import required module Write-ScriptLog 'Checking required modules...' -if (-not (Get-Module -Name 'GroupPolicy')) { - Write-ScriptLog 'Importing module: GroupPolicy' - Import-Module GroupPolicy -ErrorAction Stop -} +Import-RequiredModule -Name 'GroupPolicy' # Retrieve every GPO in the domain Write-ScriptLog 'Retrieving all GPOs...' diff --git a/Get-ServiceAccounts/Get-ServiceAccounts.md b/Get-ServiceAccounts/Get-ServiceAccounts.md index 3a9772c..da00689 100644 --- a/Get-ServiceAccounts/Get-ServiceAccounts.md +++ b/Get-ServiceAccounts/Get-ServiceAccounts.md @@ -183,12 +183,12 @@ The following tasks are filtered out **on the remote server** before results are | Version | Date | Author | Change | | --- | --- | --- | --- | -| 1.0 | 2026-06-08 | M. Stam | Initial version | -| 1.1 | 2026-06-08 | M. Stam | Added `-ComputerList` parameter and `_FailedServers.txt` export | -| 1.2 | 2026-06-08 | M. Stam | Combined per-server CSVs into a single Excel workbook at run end | -| 1.3 | 2026-06-08 | M. Stam | Added scheduled task enumeration via PSRemoting; added `Type` column | -| 1.4 | 2026-06-08 | M. Stam | Split services and tasks into separate CSVs and Excel sheets | -| 1.5 | 2026-06-08 | M. Stam | Added scheduled task folder and name-prefix exclusions | -| 1.6 | 2026-06-10 | M. Stam | Bug fixes: Excel `-Append`, CimSession, CSV tracking, logging, validation | -| 1.7 | 2026-06-10 | M. Stam | Robustness: ping/CIM timeouts, progress bar, `@()` guard, UTF8 note | | 1.8 | 2026-07-13 | M. Stam | Migrated logging to shared module `Shared\PublicScripts.psm1`. Removed local `Write-Log` implementation; all logging now routes through `Write-ScriptLog`. | +| 1.7 | 2026-06-10 | M. Stam | Robustness: ping/CIM timeouts, progress bar, `@()` guard, UTF8 note | +| 1.6 | 2026-06-10 | M. Stam | Bug fixes: Excel `-Append`, CimSession, CSV tracking, logging, validation | +| 1.5 | 2026-06-08 | M. Stam | Added scheduled task folder and name-prefix exclusions | +| 1.4 | 2026-06-08 | M. Stam | Split services and tasks into separate CSVs and Excel sheets | +| 1.3 | 2026-06-08 | M. Stam | Added scheduled task enumeration via PSRemoting; added `Type` column | +| 1.2 | 2026-06-08 | M. Stam | Combined per-server CSVs into a single Excel workbook at run end | +| 1.1 | 2026-06-08 | M. Stam | Added `-ComputerList` parameter and `_FailedServers.txt` export | +| 1.0 | 2026-06-08 | M. Stam | Initial version | diff --git a/Get-ServiceAccounts/Get-ServiceAccounts.ps1 b/Get-ServiceAccounts/Get-ServiceAccounts.ps1 index fa834d4..6ad23ec 100644 --- a/Get-ServiceAccounts/Get-ServiceAccounts.ps1 +++ b/Get-ServiceAccounts/Get-ServiceAccounts.ps1 @@ -59,6 +59,20 @@ PSRemoting (WinRM) is required for scheduled task enumeration; it is skipped gracefully when only DCOM is available. Run as an account with read access to AD and remote WMI/WinRM on all servers. + + Version history: + 1.8 2026-07-13 M. Stam Migrated logging to shared module Shared\PublicScripts.psm1. + Removed local Write-Log implementation; all logging now + routes through Write-ScriptLog. + 1.7 2026-06-10 M. Stam Robustness: ping/CIM timeouts, progress bar, @() guard, UTF8 note. + 1.6 2026-06-10 M. Stam Bug fixes: Excel -Append, CimSession, CSV tracking, logging, + validation. + 1.5 2026-06-08 M. Stam Added scheduled task folder and name-prefix exclusions. + 1.4 2026-06-08 M. Stam Split services and tasks into separate CSVs and Excel sheets. + 1.3 2026-06-08 M. Stam Added scheduled task enumeration via PSRemoting; added Type column. + 1.2 2026-06-08 M. Stam Combined per-server CSVs into a single Excel workbook at run end. + 1.1 2026-06-08 M. Stam Added -ComputerList parameter and _FailedServers.txt export. + 1.0 2026-06-08 M. Stam Initial version. #> #Requires -Modules ActiveDirectory, ImportExcel diff --git a/README.md b/README.md index 54a3423..01811f5 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ infrastructure management. | Folder | Version | Type | Description | | --- | --- | --- | --- | | [Add-Named-Instances](Add-Named-Instances/) | 2.0.0 | PowerShell | Assigns SQL Server named instance service accounts to the required local security groups for CIS hardening. Creates missing groups automatically; verifies LSA rights via `secedit`. Supports `-WhatIf`. | -| [CleanupPolicies](CleanupPolicies/) | 1.3.0 | PowerShell | Inventories Group Policy Objects that are unlinked (not applied to any OU, site or domain container) or have no 'Apply Group Policy' Allow ACE, producing an Excel/CSV report for GPO cleanup reviews. | -| [Compare-GPOsByOU](Compare-GPOsByOU/) | 1.5.0 | PowerShell | Reports all GPOs linked to each OU including link order, enabled/enforced state, WMI filter details, and orphaned GPOs. Supports DC pinning, parallel inheritance fetching on PS 7+, settings conflict detection, and `-PassThru` for pipeline use. | +| [CleanupPolicies](CleanupPolicies/) | 1.4.0 | PowerShell | Inventories Group Policy Objects that are unlinked (not applied to any OU, site or domain container) or have no 'Apply Group Policy' Allow ACE, producing an Excel/CSV report for GPO cleanup reviews. | +| [Compare-GPOsByOU](Compare-GPOsByOU/) | 1.6.0 | PowerShell | Reports all GPOs linked to each OU including link order, enabled/enforced state, WMI filter details, and orphaned GPOs. Supports DC pinning, parallel inheritance fetching on PS 7+, settings conflict detection, and `-PassThru` for pipeline use. | | [Generate-RDCManConfigs](Generate-RDCManConfigs/) | 2.1.0 | PowerShell | Generates RDCMan (`.rdg`) configuration files for one or more Active Directory domains by querying enabled Windows Server objects and organising them into OU-mirrored groups. | -| [Get-AllGPOSettings](Get-AllGPOSettings/) | 1.0.1 | PowerShell | Reports every configured setting in every GPO in a domain, regardless of where (or whether) each GPO is linked, including orphaned GPOs. Exports a single Excel/CSV "Settings" report with parallel report fetching on PS 7+ and `-PassThru` for pipeline use. | +| [Get-AllGPOSettings](Get-AllGPOSettings/) | 1.1.0 | PowerShell | Reports every configured setting in every GPO in a domain, regardless of where (or whether) each GPO is linked, including orphaned GPOs. Exports a single Excel/CSV "Settings" report with parallel report fetching on PS 7+ and `-PassThru` for pipeline use. | | [Get-ServiceAccounts](Get-ServiceAccounts/) | 1.8 | PowerShell | Scans all enabled Windows Server objects in Active Directory for non-standard accounts used by Windows services and scheduled tasks, exporting results to Excel and per-server CSV files. | -| [Shared](Shared/) | 1.0.0 | PowerShell module | Shared logging utilities (`Initialize-ScriptLog`, `Write-ScriptLog`, `Remove-OldLog`) used by all PowerShell scripts in this repository. | +| [Shared](Shared/) | 1.1.0 | PowerShell module | Shared logging (`Initialize-ScriptLog`, `Write-ScriptLog`, `Remove-OldLog`), module-import (`Import-RequiredModule`), and GPO XML report parsing (`Get-XmlInnerText`, `Get-GpoSetting`) utilities used by PowerShell scripts in this repository. | | [SQL-Create-ServiceNow-User](SQL-Create-ServiceNow-User/) | 1.0 | T-SQL | Provisions a Windows-authenticated SQL Server login and database user for the local `\servicenow` account, used by ServiceNow discovery and integration. | ## Usage diff --git a/Shared/PublicScripts.md b/Shared/PublicScripts.md index 5941318..ed0befe 100644 --- a/Shared/PublicScripts.md +++ b/Shared/PublicScripts.md @@ -1,17 +1,18 @@ # Shared/PublicScripts.psm1 -> Version 1.0.0 — 2026-07-13 +> Version 1.1.0 — 2026-09-09 ## Synopsis -Shared logging utilities used by all scripts in this repository. +Shared logging, module-import, and GPO XML report parsing utilities used by scripts in this repository. ## Description -`PublicScripts.psm1` is a PowerShell module that provides the logging infrastructure -shared by every script in this repository. Importing it once per script replaces the -per-script `Write-Log` / `Remove-OldLogs` boilerplate that had accumulated across -the collection. +`PublicScripts.psm1` is a PowerShell module that provides the logging infrastructure, +module-import helper, and GPO XML report parsing helpers shared by scripts in this +repository. Importing it once per script replaces the per-script `Write-Log` / +`Remove-OldLogs` / module-check / `Get-XmlInnerText` / `Get-GpoSetting` boilerplate that +had accumulated across the collection. The companion manifest `PublicScripts.psd1` declares compatible PowerShell versions (5.1+) and the exported functions. @@ -76,6 +77,56 @@ Supports `-WhatIf` and `-Confirm`. | `-Filter` | String | No | `*.log` | Wildcard filter passed to `Get-ChildItem`. Pass a script-specific pattern (e.g. `'*MyScript.log'`) to avoid touching other scripts' logs. | | `-RetentionDays` | Int | No | `30` | Files whose `LastWriteTime` is older than this many days are deleted. Valid range: 1–3650. | +--- + +### `Import-RequiredModule` + +Imports one or more modules if they are not already loaded in the session, logging each +import via `Write-ScriptLog`. + +| Parameter | Type | Mandatory | Default | Description | +| --- | --- | --- | --- | --- | +| `-Name` | String[] | Yes | — | One or more module names to ensure are imported. | + +**Example:** + +```powershell +Import-RequiredModule -Name 'GroupPolicy', 'ActiveDirectory' +``` + +--- + +### `Get-XmlInnerText` + +Returns the `InnerText` of a named child XML node, or `$null` when the child node is +absent. Used as a small helper by `Get-GpoSetting` (and available for other GPO XML +report parsing needs). + +| Parameter | Type | Mandatory | Default | Description | +| --- | --- | --- | --- | --- | +| `-ParentNode` | `System.Xml.XmlNode` | Yes | — | The XML node to search for a child element. | +| `-LocalName` | String | Yes | — | Local (namespace-agnostic) name of the child element to return the `InnerText` of. | + +--- + +### `Get-GpoSetting` + +Fetches the full XML report for a single GPO (`Get-GPOReport`) and flattens its +configured settings into a list of objects covering: Administrative Templates, +Security Settings (Account Policies, User Rights Assignment, Audit Policy, Security +Options, Restricted Groups, System Services), Scripts, and Windows Firewall rules. + +| Parameter | Type | Mandatory | Default | Description | +| --- | --- | --- | --- | --- | +| `-GpoGuid` | String | Yes | — | GUID of the GPO to report on. | +| `-GpoName` | String | Yes | — | Display name of the GPO, used only for warning messages on failure. | +| `-DomainFqdn` | String | Yes | — | FQDN of the domain the GPO belongs to. | +| `-Server` | String | No | — | Optional domain controller to target for the `Get-GPOReport` call. | + +**Note:** uses `Write-Warning` (not `Write-ScriptLog`) internally so it is safe to call +from `ForEach-Object -Parallel` runspaces that cannot write to the shared log file. +Used by `Compare-GPOsByOU.ps1` (`-CompareSettings`) and `Get-AllGPOSettings.ps1`. + ## Usage ```powershell @@ -107,4 +158,5 @@ Remove-OldLog -LogDirectory (Join-Path $PSScriptRoot 'Log') ` | Version | Date | Author | Changes | | --- | --- | --- | --- | +| 1.1.0 | 2026-09-09 | M. Stam | Added `Import-RequiredModule` (deduplicates the module-check-and-import pattern used across scripts), `Get-XmlInnerText`, and `Get-GpoSetting` (deduplicated from `Compare-GPOsByOU.ps1` and `Get-AllGPOSettings.ps1`). | | 1.0.0 | 2026-07-13 | M. Stam | Initial release. Extracted `Initialize-ScriptLog`, `Write-ScriptLog`, and `Remove-OldLog` from per-script boilerplate across all five scripts in the repository. | diff --git a/Shared/PublicScripts.psd1 b/Shared/PublicScripts.psd1 index 5695853..2957b64 100644 --- a/Shared/PublicScripts.psd1 +++ b/Shared/PublicScripts.psd1 @@ -1,11 +1,11 @@ @{ - ModuleVersion = '1.0.0' + ModuleVersion = '1.1.0' GUID = '03d050ae-a770-4d27-95a0-dbbb27602759' Author = 'M. Stam' - Description = 'Shared logging utilities (Initialize-ScriptLog, Write-ScriptLog, Remove-OldLog) for PublicScripts repository scripts.' + Description = 'Shared logging (Initialize-ScriptLog, Write-ScriptLog, Remove-OldLog), module-import (Import-RequiredModule), and GPO XML report parsing (Get-XmlInnerText, Get-GpoSetting) utilities for PublicScripts repository scripts.' PowerShellVersion = '5.1' RootModule = 'PublicScripts.psm1' - FunctionsToExport = @('Initialize-ScriptLog', 'Write-ScriptLog', 'Remove-OldLog') + FunctionsToExport = @('Initialize-ScriptLog', 'Write-ScriptLog', 'Remove-OldLog', 'Import-RequiredModule', 'Get-XmlInnerText', 'Get-GpoSetting') CmdletsToExport = @() AliasesToExport = @() VariablesToExport = @() diff --git a/Shared/PublicScripts.psm1 b/Shared/PublicScripts.psm1 index 8eb7d83..04e2906 100644 --- a/Shared/PublicScripts.psm1 +++ b/Shared/PublicScripts.psm1 @@ -16,6 +16,12 @@ compatible with PS 5.1 and PS 7+. Remove-OldLog Removes log files in a directory that are older than a configurable retention window. Supports -WhatIf / -Confirm. + Import-RequiredModule Imports one or more modules if not already loaded, logging each + import via Write-ScriptLog. + Get-XmlInnerText Returns the InnerText of a named child XML node, or $null when absent. + Get-GpoSetting Parses a GPO XML report (Get-GPOReport) into a flat list of + configured settings. Safe to call from ForEach-Object -Parallel + runspaces (uses Write-Warning, not Write-ScriptLog). #> Set-StrictMode -Version Latest @@ -164,3 +170,247 @@ function Remove-OldLog { Write-ScriptLog "Log rotation complete — $($old.Count) file(s) older than $RetentionDays days removed." } } + +# --------------------------------------------------------------------------- + +function Import-RequiredModule { + <# + .SYNOPSIS + Imports one or more modules if they are not already loaded. + .DESCRIPTION + Checks each named module with Get-Module and imports it (-ErrorAction Stop) only when + not already present in the session, logging the import via Write-ScriptLog. + Write-ScriptLog must be initialised before calling this function. + .PARAMETER Name + One or more module names to ensure are imported. + .EXAMPLE + Import-RequiredModule -Name 'GroupPolicy', 'ActiveDirectory' + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string[]] $Name + ) + + foreach ($module in $Name) { + if (-not (Get-Module -Name $module)) { + Write-ScriptLog "Importing module: $module" + Import-Module $module -ErrorAction Stop + } + } +} + +# --------------------------------------------------------------------------- + +function Get-XmlInnerText { + <# + .SYNOPSIS + Returns InnerText of a child XML node, or $null when absent. + .PARAMETER ParentNode + The XML node to search for a child element. + .PARAMETER LocalName + Local (namespace-agnostic) name of the child element to return the InnerText of. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [System.Xml.XmlNode] $ParentNode, + + [Parameter(Mandatory)] + [string] $LocalName + ) + $child = $ParentNode.SelectSingleNode("*[local-name()='$LocalName']") + if ($null -ne $child) { return $child.InnerText } + return $null +} + +# --------------------------------------------------------------------------- + +function Get-GpoSetting { + <# + .SYNOPSIS + Parses a GPO XML report and returns a flat list of settings. + .DESCRIPTION + Fetches the full XML report for a single GPO (Get-GPOReport) and flattens its + configured settings into a list of objects covering: Administrative Templates, + Security Settings (Account Policies, User Rights Assignment, Audit Policy, + Security Options, Restricted Groups, System Services), Scripts, and Windows + Firewall rules. + .PARAMETER GpoGuid + GUID of the GPO to report on. + .PARAMETER GpoName + Display name of the GPO, used only for warning messages on failure. + .PARAMETER DomainFqdn + FQDN of the domain the GPO belongs to. + .PARAMETER Server + Optional domain controller to target for the Get-GPOReport call. + .NOTES + Uses Write-Warning (not Write-ScriptLog) so it is safe to call from + ForEach-Object -Parallel runspaces that cannot write to the shared log file. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] $GpoGuid, + + [Parameter(Mandatory)] + [string] $GpoName, + + [Parameter(Mandatory)] + [string] $DomainFqdn, + + [Parameter()] + [string] $Server + ) + + $results = [System.Collections.Generic.List[PSCustomObject]]::new() + + try { + $reportParams = @{ + Guid = $GpoGuid + ReportType = 'Xml' + Domain = $DomainFqdn + ErrorAction = 'Stop' + } + if (-not [string]::IsNullOrEmpty($Server)) { $reportParams['Server'] = $Server } + [xml] $xml = Get-GPOReport @reportParams + } + catch { + Write-Warning "Cannot retrieve report for '$GpoName': $_" + return $results + } + + foreach ($area in @('Computer', 'User')) { + $areaNode = $xml.SelectSingleNode("//*[local-name()='$area']") + if ($null -eq $areaNode) { continue } + + # Administrative Templates + foreach ($policy in @($areaNode.SelectNodes(".//*[local-name()='Policy']"))) { + $results.Add([PSCustomObject]@{ + Area = $area + ExtensionType = 'Administrative Templates' + Category = Get-XmlInnerText $policy 'Category' + SettingName = Get-XmlInnerText $policy 'Name' + SettingState = Get-XmlInnerText $policy 'State' + SettingValue = $null + }) + } + + # Security Settings – Account Policies (password, lockout, Kerberos) + foreach ($account in @($areaNode.SelectNodes(".//*[local-name()='Account']"))) { + $value = Get-XmlInnerText $account 'SettingNumber' + if ($null -eq $value) { $value = Get-XmlInnerText $account 'SettingBoolean' } + if ($null -eq $value) { $value = Get-XmlInnerText $account 'SettingString' } + $results.Add([PSCustomObject]@{ + Area = $area + ExtensionType = 'Security Settings' + Category = 'Account Policies' + SettingName = Get-XmlInnerText $account 'Name' + SettingState = $null + SettingValue = $value + }) + } + + # Security Settings – User Rights Assignment + foreach ($ura in @($areaNode.SelectNodes(".//*[local-name()='UserRightsAssignment']"))) { + $members = @($ura.SelectNodes("*[local-name()='Member']/*[local-name()='Name']")) | + ForEach-Object { $_.InnerText } + $results.Add([PSCustomObject]@{ + Area = $area + ExtensionType = 'Security Settings' + Category = 'User Rights Assignment' + SettingName = Get-XmlInnerText $ura 'Name' + SettingState = $null + SettingValue = ($members -join '; ') + }) + } + + # Security Settings – Audit Policy + foreach ($audit in @($areaNode.SelectNodes(".//*[local-name()='AuditSetting']"))) { + $name = Get-XmlInnerText $audit 'SubcategoryName' + if ($null -eq $name) { $name = Get-XmlInnerText $audit 'Category' } + $results.Add([PSCustomObject]@{ + Area = $area + ExtensionType = 'Security Settings' + Category = 'Audit Policy' + SettingName = $name + SettingState = $null + SettingValue = Get-XmlInnerText $audit 'SettingValue' + }) + } + + # Security Settings – Security Options (registry-based policy settings) + foreach ($secOpt in @($areaNode.SelectNodes(".//*[local-name()='SecurityOptions']"))) { + $displayNode = $secOpt.SelectSingleNode( + "*[local-name()='Display']/*[local-name()='Name']") + $displayName = if ($null -ne $displayNode) { $displayNode.InnerText } ` + else { Get-XmlInnerText $secOpt 'KeyName' } + $value = Get-XmlInnerText $secOpt 'SettingNumber' + if ($null -eq $value) { $value = Get-XmlInnerText $secOpt 'SettingString' } + if ($null -eq $value) { $value = Get-XmlInnerText $secOpt 'SettingBoolean' } + $results.Add([PSCustomObject]@{ + Area = $area + ExtensionType = 'Security Settings' + Category = 'Security Options' + SettingName = $displayName + SettingState = $null + SettingValue = $value + }) + } + + # Security Settings – Restricted Groups + foreach ($rg in @($areaNode.SelectNodes(".//*[local-name()='RestrictedGroup']"))) { + $members = @($rg.SelectNodes(".//*[local-name()='Member']/*[local-name()='Name']")) | + ForEach-Object { $_.InnerText } + $results.Add([PSCustomObject]@{ + Area = $area + ExtensionType = 'Security Settings' + Category = 'Restricted Groups' + SettingName = Get-XmlInnerText $rg 'GroupName' + SettingState = $null + SettingValue = ($members -join '; ') + }) + } + + # Security Settings – System Services + foreach ($svc in @($areaNode.SelectNodes(".//*[local-name()='NTService']"))) { + $results.Add([PSCustomObject]@{ + Area = $area + ExtensionType = 'Security Settings' + Category = 'System Services' + SettingName = Get-XmlInnerText $svc 'ServiceName' + SettingState = Get-XmlInnerText $svc 'StartupMode' + SettingValue = $null + }) + } + + # Scripts (Startup / Shutdown / Logon / Logoff) + foreach ($scriptItem in @($areaNode.SelectNodes(".//*[local-name()='Script']"))) { + $results.Add([PSCustomObject]@{ + Area = $area + ExtensionType = 'Scripts' + Category = Get-XmlInnerText $scriptItem 'Type' + SettingName = Get-XmlInnerText $scriptItem 'CmdLine' + SettingState = 'Configured' + SettingValue = Get-XmlInnerText $scriptItem 'Parameters' + }) + } + + # Windows Firewall Rules (Windows Defender Firewall with Advanced Security) + foreach ($fwSection in @($areaNode.SelectNodes(".//*[local-name()='FirewallRules']"))) { + foreach ($rule in @($fwSection.SelectNodes("*[local-name()='Rule']"))) { + $results.Add([PSCustomObject]@{ + Area = $area + ExtensionType = 'Windows Firewall' + Category = Get-XmlInnerText $rule 'Profile' + SettingName = Get-XmlInnerText $rule 'Name' + SettingState = Get-XmlInnerText $rule 'Active' + SettingValue = Get-XmlInnerText $rule 'Action' + }) + } + } + } + + return $results +}