diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md index d1c0439..4ae331a 100644 --- a/.memory-bank/activeContext.md +++ b/.memory-bank/activeContext.md @@ -4,23 +4,26 @@ _Last updated: 2026-05-18 (UTC). Owner: software-engineer agent._ ## Current Focus -No active feature work. Repository is on eature/RuleCountResource but the -branch is identical to `main` (HEAD `9033220`); the name appears to be a -placeholder for a future `AADSyncRuleCount` / rule-inventory resource that -has not been started. +Shipped `AADSyncRuleCount` DSC resource on branch +`ai/aadsyncrulecount-resource` (replaces the reserved +`feature/RuleCountResource` placeholder). Report-only resource: compares +expected vs. actual sync-rule count per connector (or across all +connectors when `ConnectorName` is empty/`*`), throws from `Set()` on +drift, never remediates. New event IDs 1100/1101/1102. Most recent shipped change (PR #30, May 2026): refreshed `build.ps1`, `Resolve-Dependency.ps1` and `RequiredModules.psd1` to current Sampler -conventions. Captured in `CHANGELOG.md` under `[Unreleased]`. +conventions. ## Open Decisions -- **RuleCountResource scope** — undecided. Likely a read-only/inventory DSC - resource or a public function returning sync-rule counts per connector. No - spec drafted; do not implement speculatively. -- **Next release cut** — `[Unreleased]` only contains a build-system refresh. - Decide whether to ship as `0.5.1` (patch) or fold into the next feature - release. Default: patch, since user-visible surface is unchanged. +- **Unit tests for AADSyncRuleCount** — not yet written. ADSync cmdlets + cannot run on a build agent, so tests must mock `Get-ADSyncRule`. Mirror + the pattern used by existing class tests (none exist yet under `tests/` + for the resource classes, only `tests/QA/module.tests.ps1`). +- **Next release cut** — `[Unreleased]` now contains the new resource + plus the earlier build-system refresh; bump to `0.6.0` (minor) for the + new public surface. ## Next Steps (when work resumes) diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index f6b51c6..b52df62 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -8,21 +8,24 @@ shipped work. Older milestones are summarised; full detail lives in - **Released**: ``0.5.0`` (2025-10-17) — published to PowerShell Gallery via the standard Sampler/Azure Pipelines flow. -- **In ``[Unreleased]``**: Build-system refresh only (``build.ps1``, - ``Resolve-Dependency.ps1``, ``RequiredModules.psd1`` aligned to current - Sampler). No user-visible behaviour change. -- **Branch**: ``feature/RuleCountResource`` exists but contains zero commits - beyond ``main``. Treat as a name reservation, not work-in-progress. -- **Open work**: none active. Remaining doc-quality items (markdown lint, - link validation) are tracked below. +- **In ``[Unreleased]``**: build-system refresh + new ``AADSyncRuleCount`` + DSC resource (report-only rule-count verifier, per-connector or global). +- **Branch**: ``ai/aadsyncrulecount-resource`` — implements the previously + reserved ``feature/RuleCountResource`` scope. +- **Open work**: unit tests for ``AADSyncRuleCount`` and doc-quality + items below. + +## Shipped (recent) + +- 2026-05-18 — Added ``AADSyncRuleCount`` DSC resource. Throws from + ``Set()`` on count drift, never remediates. Event IDs 1100/1101/1102. ## Remaining Tasks -1. **Markdown lint sweep** across ``docs/`` — minor formatting drift. -2. **Link validation** for internal/external links in ``README.md`` and +1. **Unit tests** for ``AADSyncRuleCount`` (mock ``Get-ADSyncRule``). +2. **Markdown lint sweep** across ``docs/`` — minor formatting drift. +3. **Link validation** for internal/external links in ``README.md`` and ``docs/``. -3. **Decide RuleCountResource scope** before any code lands on the - feature branch. ## Recent Releases (newest first) diff --git a/CHANGELOG.md b/CHANGELOG.md index 970dbd6..3ea91c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- New DSC resource `AADSyncRuleCount` that verifies the number of + Azure AD Connect synchronization rules against an expected value. + The resource is report-only: it never attempts to remediate count + drift and instead throws a descriptive error so the LCM marks the + configuration as failed. Scope is per-connector via `ConnectorName`, + or across all connectors when `ConnectorName` is empty or `*`. + ### Changed - Updated module dependencies in `RequiredModules.psd1`. diff --git a/source/AADConnectDsc.psd1 b/source/AADConnectDsc.psd1 index e6443c5..f0d1bb8 100644 --- a/source/AADConnectDsc.psd1 +++ b/source/AADConnectDsc.psd1 @@ -79,7 +79,8 @@ # DSC resources to export from this module DscResourcesToExport = @( 'AADSyncRule', - 'AADConnectDirectoryExtensionAttribute' + 'AADConnectDirectoryExtensionAttribute', + 'AADSyncRuleCount' ) # List of all modules packaged with this module diff --git a/source/Classes/AADSyncRuleCount.ps1 b/source/Classes/AADSyncRuleCount.ps1 new file mode 100644 index 0000000..36dc13e --- /dev/null +++ b/source/Classes/AADSyncRuleCount.ps1 @@ -0,0 +1,105 @@ +[DscResource()] +class AADSyncRuleCount +{ + <# + .SYNOPSIS + Verifies the number of Azure AD Connect synchronization rules. + + .DESCRIPTION + Read-only / report-only DSC resource that compares the expected + number of AAD Connect sync rules against the current state and + reports a drift if they differ. The resource never attempts to + remediate drift because creating or removing rules to match a + count is not a meaningful operation; the user must investigate + and reconcile manually. + + Use `ConnectorName` to scope the count to a specific connector. + Leave `ConnectorName` empty (or set it to `*`) to count every + sync rule across all connectors. + #> + + # `ConnectorName` is the key. Empty string or '*' means "all connectors". + [DscProperty(Key = $true)] + [string]$ConnectorName + + [DscProperty(Mandatory = $true)] + [uint32]$RuleCount + + [DscProperty(NotConfigurable)] + [uint32]$CurrentRuleCount + + [bool]Test() + { + $currentState = $this.Get() + $scope = $this.GetScopeDescription() + + if ($currentState.CurrentRuleCount -eq $this.RuleCount) + { + Write-Verbose -Message "AADSyncRuleCount: $scope has the expected $($this.RuleCount) sync rule(s)." + $this.TryWriteEventLog('Information', 1100, "AADSyncRuleCount in desired state for $scope (count=$($this.RuleCount)).") + return $true + } + + Write-Verbose -Message "AADSyncRuleCount: $scope has $($currentState.CurrentRuleCount) sync rule(s) but expected $($this.RuleCount)." + $this.TryWriteEventLog('Warning', 1101, "AADSyncRuleCount drift for $scope. Expected=$($this.RuleCount); Current=$($currentState.CurrentRuleCount).") + return $false + } + + [AADSyncRuleCount]Get() + { + $currentState = [AADSyncRuleCount]::new() + $currentState.ConnectorName = $this.ConnectorName + $currentState.RuleCount = $this.RuleCount + + $rules = if ([string]::IsNullOrEmpty($this.ConnectorName) -or $this.ConnectorName -eq '*') + { + Get-ADSyncRule + } + else + { + Get-ADSyncRule -ConnectorName $this.ConnectorName + } + + # Get-ADSyncRule may return $null when no rules match; coerce to 0. + $currentState.CurrentRuleCount = if ($null -eq $rules) { 0 } else { @($rules).Count } + + return $currentState + } + + [void]Set() + { + # This resource is report-only. Adjusting the rule count automatically + # is not safe and not meaningful — the operator must investigate which + # rules are missing or extra. Throw a clear error so the LCM marks the + # configuration as failed. + + $currentState = $this.Get() + $scope = $this.GetScopeDescription() + $message = "AADSyncRuleCount drift detected for $scope. Expected $($this.RuleCount) sync rule(s) but found $($currentState.CurrentRuleCount). This resource does not remediate count drift; investigate the AAD Connect configuration manually." + + $this.TryWriteEventLog('Error', 1102, $message) + throw $message + } + + hidden [string]GetScopeDescription() + { + if ([string]::IsNullOrEmpty($this.ConnectorName) -or $this.ConnectorName -eq '*') + { + return "all connectors" + } + + return "connector '$($this.ConnectorName)'" + } + + hidden [void]TryWriteEventLog([string]$EventType, [int]$EventId, [string]$Message) + { + try + { + Write-AADConnectEventLog -EventType $EventType -EventId $EventId -Message $Message -ConnectorName $this.ConnectorName + } + catch + { + Write-Verbose -Message "Failed to write event log entry: $($_.Exception.Message)" + } + } +} diff --git a/source/Examples/Resources/AADSyncRuleCount/1-AADSyncRuleCount_Basic.ps1 b/source/Examples/Resources/AADSyncRuleCount/1-AADSyncRuleCount_Basic.ps1 new file mode 100644 index 0000000..560cd72 --- /dev/null +++ b/source/Examples/Resources/AADSyncRuleCount/1-AADSyncRuleCount_Basic.ps1 @@ -0,0 +1,29 @@ +<# +.EXAMPLE 1 + +Verifies that the 'contoso.com' connector has exactly 42 sync rules. +If the actual count differs, the resource throws an error during Set +and the LCM marks the configuration as failed. The resource never +attempts to add or remove rules to reach the expected count. +#> + +configuration Example_AADSyncRuleCount_Basic +{ + Import-DscResource -ModuleName AADConnectDsc + + node localhost + { + AADSyncRuleCount 'ContosoRuleCount' + { + ConnectorName = 'contoso.com' + RuleCount = 42 + } + + # Verify the total number of rules across all connectors. + AADSyncRuleCount 'TotalRuleCount' + { + ConnectorName = '*' + RuleCount = 168 + } + } +}