diff --git a/.memory-bank/activeContext.md b/.memory-bank/activeContext.md index ba29bc0..5a50e84 100644 --- a/.memory-bank/activeContext.md +++ b/.memory-bank/activeContext.md @@ -4,31 +4,52 @@ _Last updated: 2026-05-18_ ## Current Focus -Project is in **maintenance mode**. No active feature work. The module has not -been touched for an extended period (last substantive work: October 2025 -documentation pass). - -This turn: refreshed the Memory Bank to align with the current agent -definition (folder renamed `memory-bank/` → `.memory-bank/`, added -`promptHistory.md`, trimmed `activeContext.md` and `progress.md` to the -prescribed caps). +Adding a new composite resource `AADSyncRuleCounts` that wraps the report-only +`AADSyncRuleCount` DSC resource introduced in the `feature/AadsyncrulecountResource` +branch of the `AADConnectDsc` repository (working copy at `d:\a`). + +The composite mirrors the existing `AADSyncRules` / +`AADConnectDirectoryExtensionAttributes` schema-module pattern. It accepts an +array of hashtables (`ConnectorName`, `RuleCount`) and emits one +`AADSyncRuleCount` instance per item, mapping empty / `'*'` connector names to +the literal token `AllConnectors` so execution names stay unique. + +Files added/changed on branch `ai/add-aadsyncrulecounts`: + +- `source/DSCResources/AADSyncRuleCounts/AADSyncRuleCounts.psd1` +- `source/DSCResources/AADSyncRuleCounts/AADSyncRuleCounts.schema.psm1` +- `tests/Unit/DSCResources/Assets/Config/AADSyncRuleCounts.yml` +- `docs/AADSyncRuleCounts.md` +- `examples/6-AADSyncRuleCounts.ps1` +- `examples/README.md`, `README.md`, `CHANGELOG.md` updates +- This memory bank refresh ## Open Decisions -None. No pending design questions. +- Discovered (and fixed) a pre-existing bug: the module manifest was missing + `DscResourcesToExport`, which made `Get-DscResource -Module` return zero + composite resources in PowerShell 7. The build had been silently broken. +- The `AADSyncRuleCounts` compile test is enabled. It requires an + `AADConnectDsc` build that exposes `AADSyncRuleCount` (v0.6.0 of + `AADConnectDsc` or later). Local build uses the 0.6.0 build copied + from `d:\a` into `output/RequiredModules/AADConnectDsc/0.6.0/`. CI will + pick it up once `RequiredModules.psd1` (already `latest`) resolves to a + published version that ships `AADSyncRuleCount`. +- **In-process DSC parser caching**: `Get-DscResource -Module` and the DSC + keyword table are cached per process. Re-running the build in a long-lived + pwsh session that previously loaded an older `AADConnectDsc` will leave + stale keywords and make the new resource appear missing. Always run the + build in a fresh process (or `pwsh -NoProfile`) when changing the + underlying `AADConnectDsc` version. ## Next Steps (when work resumes) -1. Verify build still passes against current `AADConnectDsc` and `Sampler` - versions (`./build.ps1 -AutoRestore -Tasks test`). -2. Refresh `RequiredModules.psd1` pins if dependencies have moved. -3. Review open issues / PRs on the DscCommunity repo before any change. -4. Reconsider whether `productContext.md` should be folded into - `projectbrief.md` — it is no longer in the always-loaded set per the new - agent spec and currently lives as an on-demand topic file. +1. Wait for the `AADConnectDsc` PR (`feature/AadsyncrulecountResource`) to be + merged and a new version published, so CI can resolve `AADConnectDsc` from + the gallery instead of relying on the local 0.6.0 copy. +2. Cut a release with the `Unreleased` entry promoted to a numbered version. ## Non-Goals -- No new composite resources planned. -- No restructuring of the build pipeline. -- No migration off Sampler / ModuleBuilder. +- No additional composite resources planned. +- No restructuring of the build pipeline. \ No newline at end of file diff --git a/.memory-bank/progress.md b/.memory-bank/progress.md index b3ade26..c92d308 100644 --- a/.memory-bank/progress.md +++ b/.memory-bank/progress.md @@ -20,6 +20,19 @@ _Last updated: 2026-05-18_ ## Recent Log +- 2026-05-18 — Re-enabled the `AADSyncRuleCounts` compile test now that the + locally-built `AADConnectDsc 0.6.0` (with `AADSyncRuleCount`) is staged + under `output/RequiredModules/AADConnectDsc/0.6.0/`. Full build in a fresh + pwsh session passes 8/8 tests. Documented the in-process DSC parser + keyword-cache pitfall in `activeContext.md`. +- 2026-05-18 — Fixed the broken build: added missing `DscResourcesToExport` + to the module manifest (root cause: `Get-DscResource -Module` returned 0, + so the per-resource compile tests never ran and the Final tests failed), + and skipped the `AADSyncRuleCounts` compile test until `AADConnectDsc` + publishes `AADSyncRuleCount`. Full default build now exits 0. +- 2026-05-18 — Added `AADSyncRuleCounts` composite resource on branch + `ai/add-aadsyncrulecounts` wrapping the new report-only `AADSyncRuleCount` + resource from `AADConnectDsc` (`feature/AadsyncrulecountResource`). - 2026-05-18 — Memory Bank refreshed: folder renamed to `.memory-bank/`, files trimmed to new agent-spec caps, `promptHistory.md` added. - 2025-10 — Documentation pass: README cross-links to `docs/`, examples diff --git a/.memory-bank/promptHistory.md b/.memory-bank/promptHistory.md new file mode 100644 index 0000000..0882a87 --- /dev/null +++ b/.memory-bank/promptHistory.md @@ -0,0 +1,8 @@ +# Prompt History + +A one-line entry per substantive Copilot turn. Format: +`YYYY-MM-DD HH:mm UTC | agent | one-line intent` + +2026-05-18 09:53 UTC | software-engineer | Add AADSyncRuleCounts composite wrapping new AADSyncRuleCount resource from AADConnectDsc +2026-05-18 10:06 UTC | software-engineer | Fix broken build: add DscResourcesToExport to manifest; skip AADSyncRuleCounts test until AADConnectDsc ships AADSyncRuleCount +2026-05-18 10:30 UTC | software-engineer | Re-enable AADSyncRuleCounts test against local AADConnectDsc 0.6.0; root cause of false failures was per-process DSC keyword caching diff --git a/.memory-bank/systemPatterns.md b/.memory-bank/systemPatterns.md index 3000dcb..75192ae 100644 --- a/.memory-bank/systemPatterns.md +++ b/.memory-bank/systemPatterns.md @@ -19,21 +19,21 @@ processing. ▼ ┌─────────────────────────────────────────────────────────────────┐ │ DscConfig.AADConnect │ -│ ┌─────────────────┐ ┌─────────────────────┐ │ -│ │ AADSyncRules │ │ AADConnectDirectory │ │ -│ │ Composite │ │ ExtensionAttributes │ │ -│ │ Resource │ │ Composite Resource │ │ -│ └─────────────────┘ └─────────────────────┘ │ +│ ┌────────────────┐ ┌──────────────────────┐ ┌──────────────┐ │ +│ │ AADSyncRules │ │ AADConnectDirectory │ │ AADSyncRule │ │ +│ │ Composite │ │ ExtensionAttributes │ │ Counts │ │ +│ │ Resource │ │ Composite Resource │ │ Composite │ │ +│ └────────────────┘ └──────────────────────┘ └──────────────┘ │ └─────────────────────────┬───────────────────────────────────────┘ │ Individual Resource Instances ▼ ┌─────────────────────────────────────────────────────────────────┐ │ AADConnectDsc │ -│ ┌─────────────────┐ ┌─────────────────────┐ │ -│ │ AADSyncRule │ │ AADConnectDirectory │ │ -│ │ DSC Resource │ │ ExtensionAttribute │ │ -│ │ │ │ DSC Resource │ │ -│ └─────────────────┘ └─────────────────────┘ │ +│ ┌────────────────┐ ┌──────────────────────┐ ┌──────────────┐ │ +│ │ AADSyncRule │ │ AADConnectDirectory │ │ AADSyncRule │ │ +│ │ DSC Resource │ │ ExtensionAttribute │ │ Count │ │ +│ │ │ │ DSC Resource │ │ (report-only)│ │ +│ └────────────────┘ └──────────────────────┘ └──────────────┘ │ └─────────────────────────┬───────────────────────────────────────┘ │ ADSync PowerShell Module ▼ @@ -135,6 +135,17 @@ $executionName = ($item.ConnectorName + '__' + $item.Name) -replace '[\s(){}/\\: $executionName = ($item.Name + '__' + $item.AssignedObjectClass) -replace '[\s(){}/\\:-]', '_' ``` +**AADSyncRuleCounts Pattern** (empty / `'*'` connector → `AllConnectors`): + +```powershell +$scope = if ([string]::IsNullOrEmpty($item.ConnectorName) -or $item.ConnectorName -eq '*') { + 'AllConnectors' +} else { + $item.ConnectorName +} +$executionName = ("AADSyncRuleCount__$scope") -replace '[\s(){}/\\:-]', '_' +``` + ### Integration Patterns #### Configuration Management Integration diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd7ac9..4f8d9c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- New composite resource `AADSyncRuleCounts` that wraps the report-only + `AADSyncRuleCount` DSC resource from `AADConnectDsc`. Accepts an array of + hashtables (`ConnectorName`, `RuleCount`) and generates one underlying + resource instance per item with a safe execution name. The empty / `'*'` + connector value is mapped to the literal token `AllConnectors` so + execution names stay unique across items. + Requires `AADConnectDsc` with `AADSyncRuleCount` support. + ### Changed - Updated module dependencies in `RequiredModules.psd1`. - Updated build scripts (`build.ps1`, `Resolve-Dependency.ps1`) to align with the latest version of Sampler. +### Fixed + +- Added the missing `DscResourcesToExport` entry to + `source/DscConfig.AADConnect.psd1` so `Get-DscResource -Module + DscConfig.AADConnect` returns the composite resources. Without it, the + `tests/Unit/DSCResources/DscResources.Tests.ps1` discovery returned zero + resources, the per-resource compile tests were never generated, and the + `Final tests` count comparison failed in PowerShell 7. The build is now + green again. + ## [0.2.0] - 2025-10-16 ### Added diff --git a/README.md b/README.md index 8aaac2a..d41812b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,16 @@ Processes arrays of directory extension attribute configurations and generates i - Schema validation and type checking - Integration with Azure AD schema requirements +### AADSyncRuleCounts + +Processes arrays of sync-rule-count expectations and generates individual `AADSyncRuleCount` DSC resource instances. The underlying resource is **report-only**: it never adds or removes rules to reach the expected count, it fails the configuration when the actual count diverges so an operator can investigate. + +**Key Features:** +- Bulk drift detection for sync-rule counts per connector +- Single-scope or all-connector (`'*'`) checks in the same array +- Safe execution-name generation for the empty / wildcard connector case +- Integration with the same `AADConnectDsc` runtime as the other composites + ## Requirements ### System Requirements @@ -175,6 +185,7 @@ For detailed documentation on each composite resource, see: - **[AADSyncRules](docs/AADSyncRules.md)**: Processes arrays of Azure AD Connect sync rule configurations - **[AADConnectDirectoryExtensionAttributes](docs/AADConnectDirectoryExtensionAttributes.md)**: Processes arrays of directory extension attribute configurations +- **[AADSyncRuleCounts](docs/AADSyncRuleCounts.md)**: Processes arrays of sync-rule-count expectations (report-only drift detection) ### Quick Reference @@ -201,6 +212,18 @@ Execution names are generated using the pattern: `{ConnectorName}__{RuleName}` w **Execution Name Generation:** Execution names are generated using the pattern: `{AttributeName}__{ObjectClass}` with special characters replaced by underscores. +#### AADSyncRuleCounts + +**Parameters:** + +- **Items** (Mandatory): Array of hashtables representing expected sync-rule counts + - Each hashtable must contain `ConnectorName` (key) and `RuleCount` (mandatory `uint32`) + - Use an empty string or `'*'` for `ConnectorName` to count rules across all connectors + - The underlying `AADSyncRuleCount` resource is report-only; it does not remediate count drift + +**Execution Name Generation:** +Execution names use the pattern `AADSyncRuleCount__{ConnectorName}` with special characters replaced by underscores. The empty / `'*'` connector value is mapped to the literal token `AllConnectors` to keep execution names unique and valid. + ## Examples For additional examples and advanced usage scenarios, see the [Examples](examples/) directory. diff --git a/docs/AADSyncRuleCounts.md b/docs/AADSyncRuleCounts.md new file mode 100644 index 0000000..c2b0396 --- /dev/null +++ b/docs/AADSyncRuleCounts.md @@ -0,0 +1,140 @@ +# AADSyncRuleCounts Composite Resource + +## Description + +The `AADSyncRuleCounts` composite resource processes arrays of Azure AD Connect +sync-rule-count expectations and generates individual `AADSyncRuleCount` DSC +resource instances. It is intended for bulk drift detection: each item declares +the expected number of sync rules for a given connector (or across all +connectors) and the underlying [AADSyncRuleCount](https://github.com/dsccommunity/AADConnectDsc) resource reports a configuration failure +when the actual count diverges from the expected count. + +> [!NOTE] +> The underlying `AADSyncRuleCount` resource is **report-only**. It does not +> create or remove sync rules to reach the expected count. When drift is +> detected the LCM marks the configuration as failed and the operator must +> investigate manually. + +## Parameters + +### Items + +- **Type**: `hashtable[]` +- **Required**: Yes +- **Description**: Array of hashtables describing the expected sync-rule counts. + +Each hashtable must contain the parameters required by the underlying +`AADSyncRuleCount` resource: + +| Property | Type | Required | Description | +|-----------------|----------|----------|-------------| +| `ConnectorName` | `string` | Yes (key) | Name of the AAD Connect connector to scope the count to. Use an empty string or `'*'` to count rules across **all** connectors. | +| `RuleCount` | `uint32` | Yes | The expected number of sync rules for the scope. | + +## Behavior + +### Execution Name Generation + +Execution names are generated using the pattern: + +```text +AADSyncRuleCount__{scope} +``` + +Where `{scope}` is the value of `ConnectorName`, except that an empty string or +`'*'` is mapped to the literal token `AllConnectors` so the name remains a +valid, unique resource identifier. Special characters (whitespace, brackets, +slashes, colons, dashes) are replaced with `_` using the regex pattern +`[\s(){}/\\:-]`. + +Examples: + +| `ConnectorName` value | Generated execution name | +|-----------------------|---------------------------------------| +| `contoso.com` | `AADSyncRuleCount__contoso.com` | +| `fabrikam.com` | `AADSyncRuleCount__fabrikam.com` | +| `''` (empty) | `AADSyncRuleCount__AllConnectors` | +| `*` | `AADSyncRuleCount__AllConnectors` | + +### Resource Delegation + +Each item is passed to a single `AADSyncRuleCount` resource instance via the +`Get-DscSplattedResource` utility. The composite performs no validation beyond +ensuring the items are processable; the underlying resource is responsible for +key/type validation. + +## Examples + +### Example 1: Per-connector count check + +```powershell +Configuration BasicRuleCounts { + Import-DscResource -ModuleName DscConfig.AADConnect + + Node localhost { + AADSyncRuleCounts 'CompanyRuleCounts' { + Items = @( + @{ ConnectorName = 'contoso.com'; RuleCount = 42 } + @{ ConnectorName = 'fabrikam.com'; RuleCount = 30 } + ) + } + } +} +``` + +### Example 2: Total count across all connectors + +```powershell +Configuration TotalRuleCount { + Import-DscResource -ModuleName DscConfig.AADConnect + + Node localhost { + AADSyncRuleCounts 'TotalCount' { + Items = @( + @{ ConnectorName = '*'; RuleCount = 168 } + ) + } + } +} +``` + +### Example 3: Configuration-management integration + +```yaml +# Datum / DscWorkshop configuration data +AADSyncRuleCounts: + Items: + - ConnectorName: contoso.com + RuleCount: 42 + - ConnectorName: fabrikam.com + RuleCount: 30 + - ConnectorName: '*' + RuleCount: 168 +``` + +```powershell +Configuration DataDrivenRuleCounts { + Import-DscResource -ModuleName DscConfig.AADConnect + + Node $AllNodes.NodeName { + AADSyncRuleCounts 'RuleCounts' { + Items = $ConfigurationData.AADSyncRuleCounts.Items + } + } +} +``` + +## Related Resources + +- [AADSyncRuleCount](https://github.com/dsccommunity/AADConnectDsc) — the + underlying report-only DSC resource provided by `AADConnectDsc`. +- [AADSyncRules](AADSyncRules.md) — companion composite resource that manages + the sync rules themselves. + +## Notes + +- This composite resource runs during DSC configuration compilation. +- The companion `AADSyncRuleCount` resource ships with `AADConnectDsc` + starting with the version that introduces report-only count drift detection. + If your installed `AADConnectDsc` predates that version, compilation will + fail because the underlying resource is not present. \ No newline at end of file diff --git a/examples/6-AADSyncRuleCounts.ps1 b/examples/6-AADSyncRuleCounts.ps1 new file mode 100644 index 0000000..a484948 --- /dev/null +++ b/examples/6-AADSyncRuleCounts.ps1 @@ -0,0 +1,43 @@ +<# +.EXAMPLE 6 + +This example demonstrates the AADSyncRuleCounts composite resource which +wraps the report-only AADSyncRuleCount DSC resource from AADConnectDsc. + +The resource verifies that each connector has the expected number of sync +rules. It does not create or remove sync rules to reach the expected count; +when drift is detected the LCM marks the configuration as failed and the +operator must investigate. +#> + +configuration Example_DscConfig_AADSyncRuleCounts +{ + Import-DscResource -ModuleName DscConfig.AADConnect + + node localhost + { + $ruleCounts = @( + @{ + ConnectorName = 'contoso.com' + RuleCount = 42 + }, + @{ + ConnectorName = 'fabrikam.com' + RuleCount = 30 + }, + # Empty ConnectorName or '*' counts rules across all connectors. + @{ + ConnectorName = '*' + RuleCount = 168 + } + ) + + AADSyncRuleCounts 'CompanyRuleCounts' + { + Items = $ruleCounts + } + } +} + +# Compile the configuration +Example_DscConfig_AADSyncRuleCounts -OutputPath '.\Output' \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 8c8bdb2..1571eca 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,6 +24,10 @@ Example showing how to use external YAML/JSON data sources with the composite re Complete example demonstrating integration with DscWorkshop and Datum frameworks, including hierarchical configuration data, merge strategies, and enterprise patterns. +### [6-AADSyncRuleCounts.ps1](6-AADSyncRuleCounts.ps1) + +Demonstrates the report-only `AADSyncRuleCounts` composite resource for bulk drift detection of expected sync-rule counts per connector and across all connectors. + ## Usage Each example includes: diff --git a/source/DSCResources/AADSyncRuleCounts/AADSyncRuleCounts.psd1 b/source/DSCResources/AADSyncRuleCounts/AADSyncRuleCounts.psd1 new file mode 100644 index 0000000..31683c3 --- /dev/null +++ b/source/DSCResources/AADSyncRuleCounts/AADSyncRuleCounts.psd1 @@ -0,0 +1,15 @@ +@{ + RootModule = 'AADSyncRuleCounts.schema.psm1' + + ModuleVersion = '0.0.1' + + GUID = 'c67a927b-46e8-443e-90d9-5d90e5312c35' + + Author = 'NA' + + CompanyName = 'NA' + + Copyright = 'NA' + + DscResourcesToExport = @('AADSyncRuleCounts') +} \ No newline at end of file diff --git a/source/DSCResources/AADSyncRuleCounts/AADSyncRuleCounts.schema.psm1 b/source/DSCResources/AADSyncRuleCounts/AADSyncRuleCounts.schema.psm1 new file mode 100644 index 0000000..1ec08ce --- /dev/null +++ b/source/DSCResources/AADSyncRuleCounts/AADSyncRuleCounts.schema.psm1 @@ -0,0 +1,29 @@ +configuration AADSyncRuleCounts { + param + ( + [Parameter(Mandatory = $true)] + [hashtable[]] + $Items + ) + + Import-DscResource -ModuleName AADConnectDsc + Import-DscResource -ModuleName PSDesiredStateConfiguration + + foreach ($item in $Items) + { + # ConnectorName is the key. Empty string or '*' means "all connectors". + # Translate that to a safe, descriptive token for the execution name so + # individual array items remain uniquely identifiable in compiled MOF. + $scope = if ([string]::IsNullOrEmpty($item.ConnectorName) -or $item.ConnectorName -eq '*') + { + 'AllConnectors' + } + else + { + $item.ConnectorName + } + + $executionName = ("AADSyncRuleCount__$scope") -replace '[\s(){}/\\:-]', '_' + (Get-DscSplattedResource -ResourceName AADSyncRuleCount -ExecutionName $executionName -Properties $item -NoInvoke).Invoke($item) + } +} diff --git a/source/DscConfig.AADConnect.psd1 b/source/DscConfig.AADConnect.psd1 index 3bc4646..2de7da7 100644 --- a/source/DscConfig.AADConnect.psd1 +++ b/source/DscConfig.AADConnect.psd1 @@ -12,6 +12,12 @@ VariablesToExport = '*' AliasesToExport = '*' + DscResourcesToExport = @( + 'AADSyncRules' + 'AADConnectDirectoryExtensionAttributes' + 'AADSyncRuleCounts' + ) + PrivateData = @{ PSData = @{ diff --git a/tests/Unit/DSCResources/Assets/Config/AADSyncRuleCounts.yml b/tests/Unit/DSCResources/Assets/Config/AADSyncRuleCounts.yml new file mode 100644 index 0000000..a6a3e4c --- /dev/null +++ b/tests/Unit/DSCResources/Assets/Config/AADSyncRuleCounts.yml @@ -0,0 +1,7 @@ +Items: + - ConnectorName: contoso.com + RuleCount: 42 + - ConnectorName: fabrikam.com + RuleCount: 30 + - ConnectorName: '*' + RuleCount: 168 \ No newline at end of file