Skip to content
Draft
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
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- UpdateServicesServer
- BREAKING CHANGE: All parameters will now only be set when specifically applied
rather than defaulting to hardcoded values if left undefined.
In particular set ContentDir, Languages, Products, Classifications as needed.
Fixes [issue #55](https://github.com/dsccommunity/UpdateServicesDsc/issues/55)

### Added

- UpdateServicesServer
- Added support for the following settings:
- ContentDir can be set to empty string for clients to download from Microsoft Update.
- Updates are downloaded only when they are approved.
- Express installation packages should be downloaded.
- Update binaries are downloaded from Microsoft Update instead of from the
upstream server.
Fixes [issue #39](https://github.com/dsccommunity/UpdateServicesDsc/issues/39)
- WSUS infrastructure updates are approved automatically.
- The latest revision of an update should be approved automatically.
- An update should be automatically declined when it is revised to be expired
and AutoRefreshUpdateApprovals is enabled.
- The downstream server should roll up detailed computer and update status information.
- Email status notifications and SMTP settings, including status notifications DST fix.
Fixes [issue #15](https://github.com/dsccommunity/UpdateServicesDsc/issues/15)
- Use Xpress Encoding to compress update metadata.
- Use foreground priority for BITS downloads
- The maximum .cab file size (in megabytes) that Local Publishing will create.
- The maximum number of concurrent update downloads.

### Fixed

- UpdateServicesApprovalRule
- Before running, ensure that UpdateServices PowerShell module is installed.
- Updated error handling to specifically catch errors if WSUS Server is unavailable.
- Added check to make sure Post Install was successful before trying to get resource.
- Fix issue [#63](https://github.com/dsccommunity/UpdateServicesDsc/issues/63)
Broken verbose output for WSUS server name.
- Fix issue [#61](https://github.com/dsccommunity/UpdateServicesDsc/issues/61)
Allow multiple product categories with same name (e.g. "Windows Admin Center")
- Removed ErrorRecord from New-InvalidOperationException outside of try / catch.
- Fixed verbose logging to use language strings.
- UpdateServicesCleanup
- Fix issue [#93](https://github.com/dsccommunity/UpdateServicesDsc/issues/93)
Allow UpdateServicesCleanup resource to test and update TimeOfDay as needed.
- UpdateServicesComputerTargetGroup
- Before running, ensure that UpdateServices PowerShell module is installed.
- Updated error handling to specifically catch errors if WSUS Server is unavailable.
- Added check to make sure Post Install was successful before trying to get resource.
- UpdateServicesServer
- Before running, ensure that UpdateServices PowerShell module is installed.
- Updated error handling to specifically catch errors if WSUS Server is unavailable.
- Added check to make sure Post Install was successful before trying to get resource.
- Update setting dependency logic to stop incompatible settings being set / returned.
- Get Languages as a string array instead of comma-separated values.
Fix issue [#76](https://github.com/dsccommunity/UpdateServicesDsc/issues/76)
- Stopped PDT.psm1 returning boolean 'true' alongside normal output when creating a process.

## [1.3.0] - 2025-12-05

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,29 @@ function Get-TargetResource
$Name
)

Assert-Module -ModuleName UpdateServices

try
{
$WsusServer = Get-WsusServer
$Ensure = 'Absent'
$Classifications = $null
$Products = $null
$ComputerGroups = $null
$Enabled = $null
}
catch
{
Write-Verbose -Message $script:localizedData.GetWsusServerFailed
}

$Ensure = 'Absent'
$Classifications = $null
$Products = $null
$ComputerGroups = $null
$Enabled = $null

if ($null -ne $WsusServer)
try {
if (($null -ne $WsusServer) -and `
(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup\Installed Role Services" `
-Name 'UpdateServices-Services' -ErrorAction Stop).'UpdateServices-Services' -eq '2')
{
Write-Verbose -Message ('Identified WSUS server information: {0}' -f $WsusServer.Name)
Write-Verbose -Message ($script:localizedData.IdentifiedWsusServer -f $WsusServer.Name)

$ApprovalRule = $WsusServer.GetInstallApprovalRules() | Where-Object -FilterScript { $_.Name -eq $Name }

Expand Down Expand Up @@ -85,7 +96,7 @@ function Get-TargetResource
}
else
{
Write-Verbose -Message 'Did not identify an instance of WSUS'
Write-Verbose -Message $script:localizedData.NotIdentifiedWsusServer
}
}
catch
Expand Down Expand Up @@ -177,6 +188,8 @@ function Set-TargetResource
$RunRuleNow
)

Assert-Module -ModuleName UpdateServices

try
{
if ($WsusServer = Get-WsusServer)
Expand Down Expand Up @@ -220,11 +233,14 @@ function Set-TargetResource
$ApprovalRule.Save()

$ProductCollection = New-Object -TypeName Microsoft.UpdateServices.Administration.UpdateCategoryCollection
$AllWsusProducts = $WsusServer.GetUpdateCategories()
foreach ($Product in $Products)
{
if ($WsusProduct = Get-WsusProduct | Where-Object -FilterScript { $_.Product.Title -eq $Product })
if ($WsusProduct = $AllWsusProducts | Where-Object -FilterScript { $_.Title -eq $Product })
{
$ProductCollection.Add($WsusServer.GetUpdateCategory($WsusProduct.Product.Id))
$WsusProduct | Foreach-Object {
$ProductCollection.Add($_)
}
}
}

Expand Down Expand Up @@ -262,7 +278,7 @@ function Set-TargetResource
{
New-InvalidOperationException -Message (
$script:localizedData.RuleFailedToCreate -f $Name
) -ErrorRecord $_
)
}
}
'Absent'
Expand Down Expand Up @@ -386,6 +402,8 @@ function Test-TargetResource
$RunRuleNow
)

Assert-Module -ModuleName UpdateServices

$result = $true

$ApprovalRule = Get-TargetResource -Name $Name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ RunApprovalRule = Running approval rule {0}.
SyncWsus = Synchronizing WSUS.
ClassificationNotFound = Classification {0} not found.
GetWsusServerFailed = Get-WsusServer failed.
IdentifiedWsusServer = Identified WSUS server information: {0}
NotIdentifiedWsusServer = Did not identify an instance of WSUS
WSUSConfigurationFailed = WSUS approval rule configuration failed.
RuleFailedToCreate = Failed to create approval rule {0}.
RuleFailedToApply = Failed to apply approval rule {0}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function Get-TargetResource
}
}
}
$TimeOfDay = $Task.Triggers.StartBoundary.Split('T')[1]
$TimeOfDay = ([datetimeoffset]$Task.Triggers[0].StartBoundary).TimeOfDay.ToString('c')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for potential null reference and parsing errors.

The code accesses $Task.Triggers[0] without verifying the array has elements and casts StartBoundary to [datetimeoffset] without error handling. Either condition could cause runtime errors.

Consider wrapping this in a try-catch block or adding validation:

-            $TimeOfDay = ([datetimeoffset]$Task.Triggers[0].StartBoundary).TimeOfDay.ToString('c')
+            if ($Task.Triggers -and $Task.Triggers.Count -gt 0)
+            {
+                try
+                {
+                    $TimeOfDay = ([datetimeoffset]$Task.Triggers[0].StartBoundary).TimeOfDay.ToString('c')
+                }
+                catch
+                {
+                    Write-Verbose -Message "Failed to parse TimeOfDay from scheduled task trigger."
+                }
+            }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In source/DSCResources/DSC_UpdateServicesCleanup/DSC_UpdateServicesCleanup.psm1
around line 69, the code directly indexes $Task.Triggers[0] and casts
StartBoundary to [datetimeoffset] which can throw null-reference or parse
exceptions; add validation to ensure $Task.Triggers is not null/empty and that
$Task.Triggers[0].StartBoundary is populated, and wrap the cast/parsing in a
try/catch (or use [datetimeoffset]::TryParse) to safely obtain TimeOfDay,
logging or defaulting when validation/parse fails so the function won’t error at
runtime.

}
else
{
Expand Down Expand Up @@ -363,6 +363,12 @@ function Test-TargetResource
Write-Verbose -Message $script:localizedData.CleanupPublishedTestFailed
$result = $false
}

if ($CleanupTask.TimeOfDay -ne $TimeOfDay)
{
Write-Verbose -Message $script:localizedData.TimeOfDayTestFailed
$result = $false
}
}

$result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ CompressTestFailed = Compress Updates test failed.
CleanupObsoleteCptTestFailed= Cleanup Obsolete Computers test failed.
CleanupContentTestFailed = Cleanup Unneeded Content Files test failed.
CleanupPublishedTestFailed = Cleanup Local Published Content Files test failed.
TimeOfDayTestFailed = Time of Day test failed.
TestFailedAfterSet = Test-TargetResource returned false after calling set.
'@
Loading
Loading