Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 32 additions & 32 deletions Add-Named-Instances/Add-Named-Instances.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions CleanupPolicies/Cleanup-Policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
24 changes: 10 additions & 14 deletions CleanupPolicies/Cleanup-Policies.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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).
Expand Down Expand Up @@ -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..."
Expand Down
1 change: 1 addition & 0 deletions Compare-GPOsByOU/Compare-GPOsByOU.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,object>` + `TryGetValue`; `HashSet<string>` 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 |
Expand Down
191 changes: 7 additions & 184 deletions Compare-GPOsByOU/Compare-GPOsByOU.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 }
Expand All @@ -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<string,object> enables single-lookup TryGetValue
Write-ScriptLog 'Retrieving all GPOs...'
Expand Down
Loading
Loading