Skip to content
Open
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
25 changes: 14 additions & 11 deletions .memory-bank/activeContext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
25 changes: 14 additions & 11 deletions .memory-bank/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +25 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use 1/1/1 ordered-list numbering to satisfy markdown rules.

This section currently uses 1., 2., 3. and triggers MD029 warnings.

✅ Minimal fix
-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
+1. **Unit tests** for ``AADSyncRuleCount`` (mock ``Get-ADSyncRule``).
+1. **Markdown lint sweep** across ``docs/`` — minor formatting drift.
+1. **Link validation** for internal/external links in ``README.md`` and

As per coding guidelines, Markdown requires "Use '1.' for all items in ordered lists (1/1/1 numbering style)".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
1. **Unit tests** for ``AADSyncRuleCount`` (mock ``Get-ADSyncRule``).
1. **Markdown lint sweep** across ``docs/`` — minor formatting drift.
1. **Link validation** for internal/external links in ``README.md`` and
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 26-26: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


[warning] 27-27: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.memory-bank/progress.md around lines 25 - 27, Change the three numbered
list items so they all use the 1/1/1 ordered-list numbering style: replace the
leading "1.", "2.", "3." before "Unit tests for AADSyncRuleCount (mock
Get-ADSyncRule)", "Markdown lint sweep across docs/ — minor formatting drift",
and "Link validation for internal/external links in README.md and" so each line
begins with "1." to satisfy MD029 and the project's Markdown guideline.

``docs/``.
3. **Decide RuleCountResource scope** before any code lands on the
feature branch.

## Recent Releases (newest first)

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
3 changes: 2 additions & 1 deletion source/AADConnectDsc.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
# DSC resources to export from this module
DscResourcesToExport = @(
'AADSyncRule',
'AADConnectDirectoryExtensionAttribute'
'AADConnectDirectoryExtensionAttribute',
'AADSyncRuleCount'
)

# List of all modules packaged with this module
Expand Down
105 changes: 105 additions & 0 deletions source/Classes/AADSyncRuleCount.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
[DscResource()]
class AADSyncRuleCount
{
Comment on lines +1 to +3
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file being reviewed
cat -n source/Classes/AADSyncRuleCount.ps1

Repository: dsccommunity/AADConnectDsc

Length of output: 4633


🏁 Script executed:

# Check if ResourceBase exists in the module
fd -type f "ResourceBase" source/

Repository: dsccommunity/AADConnectDsc

Length of output: 241


🏁 Script executed:

# Check for existing DSC resources in the module to understand the pattern
find source/Classes -name "*.ps1" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -15 {}'

Repository: dsccommunity/AADConnectDsc

Length of output: 1810


Restructure resource to follow the required DSC class pattern.

The resource deviates from mandatory guidelines: missing RunAsCredential decoration, ResourceBase inheritance, required constructor, and base method calls. Additionally, hardcoded strings violate localization requirements, and line 81 uses throw instead of New-*Exception for error handling.

Per coding guidelines:

  • Line 1: Change [DscResource()] to [DscResource(RunAsCredential = 'Optional')]
  • Line 2: Add : ResourceBase inheritance and implement required constructor
  • Lines 31-67, 69-82: Restructure Test(), Get(), Set() to call base class implementations
  • Lines 38, 43, 78, 81, 102: Localize hardcoded Write-Verbose and error messages
  • Line 81: Replace throw $message with appropriate New-*Exception command
🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] Missing BOM encoding for non-ASCII encoded file 'AADSyncRuleCount.ps1'

(PSUseBOMForUnicodeEncodedFile)


[info] 1-105: No examples found for resource 'AADSyncRuleCount'

(PSDSCDscExamplesPresent)


[info] 1-105: No tests found for resource 'AADSyncRuleCount'

(PSDSCDscTestsPresent)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/Classes/AADSyncRuleCount.ps1` around lines 1 - 3, Update the
AADSyncRuleCount DSC resource to follow the DSC class pattern: change the class
attribute to [DscResource(RunAsCredential = 'Optional')], inherit from
ResourceBase (class AADSyncRuleCount : ResourceBase) and implement the required
constructor that calls the base ctor; refactor the Test(), Get(), and Set()
methods to call the corresponding base class implementations (e.g., base.Test(),
base.Get(), base.Set()) as part of their workflows; replace hardcoded
Write-Verbose and error strings in methods (lines referenced in the review) with
localized messages (use the existing localization mechanism used in other
resources); and replace the raw throw $message on failure in Set() with the
appropriate New-*Exception cmdlet (e.g., New-ItemStateChangeException or the
project-standard New-*-Exception helper) while including the localized message.

<#
.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).")
Comment on lines +38 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Localize all user-facing and diagnostic messages.

Hardcoded strings are used in Write-Verbose and drift/event messages (Line 38, Line 43, Line 78, Line 102). These should come from localized string keys.

As per coding guidelines, Localize all Write-Debug, Write-Verbose, Write-Error, Write-Warning and $PSCmdlet.ThrowTerminatingError() messages and Use localized string keys, not hardcoded strings.

Also applies to: 78-78, 102-102

🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] Missing BOM encoding for non-ASCII encoded file 'AADSyncRuleCount.ps1'

(PSUseBOMForUnicodeEncodedFile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/Classes/AADSyncRuleCount.ps1` around lines 38 - 44, Replace hardcoded
user-facing and diagnostic strings in the AADSyncRuleCount class (the
Write-Verbose calls and TryWriteEventLog invocations that produce the
information/warning messages) with localized string keys and lookup calls; e.g.,
introduce/consume a localization helper (Get-LocalizedString or $this.Localize)
and pass the RuleCount, scope, and CurrentRuleCount as format arguments to that
helper instead of embedding values in literal strings, and update the other
occurrences noted (the messages at the other Write-Verbose/TryWriteEventLog
sites referenced in the comment) to use the same localization keys and helper so
all Write-Debug/Write-Verbose/Write-Error/Write-Warning and event log messages
are sourced from localized resources.

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
}
Comment on lines +80 to +82
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

head -100 source/Classes/AADSyncRuleCount.ps1 | cat -n

Repository: dsccommunity/AADConnectDsc

Length of output: 4476


🏁 Script executed:

wc -l source/Classes/AADSyncRuleCount.ps1

Repository: dsccommunity/AADConnectDsc

Length of output: 110


🏁 Script executed:

sed -n '70,90p' source/Classes/AADSyncRuleCount.ps1 | cat -n

Repository: dsccommunity/AADConnectDsc

Length of output: 1107


🏁 Script executed:

grep -n "throw " source/Classes/AADSyncRuleCount.ps1

Repository: dsccommunity/AADConnectDsc

Length of output: 96


🏁 Script executed:

# Check if New-InvalidOperationException is available/used in this codebase
find source -name "*.ps1" -type f -exec grep -l "New-InvalidOperationException" {} \;

Repository: dsccommunity/AADConnectDsc

Length of output: 52


🏁 Script executed:

# Check what DscResource.Common functions are imported or referenced
grep -r "New-.*Exception" source --include="*.ps1" | head -20

Repository: dsccommunity/AADConnectDsc

Length of output: 52


🏁 Script executed:

# Check the module manifest for DscResource.Common dependencies
cat source/*.psd1 | grep -A 20 "RequiredModules"

Repository: dsccommunity/AADConnectDsc

Length of output: 947


🏁 Script executed:

# Check if there are any imports at the top of the file
head -20 source/Classes/AADSyncRuleCount.ps1

Repository: dsccommunity/AADConnectDsc

Length of output: 848


🏁 Script executed:

# Check if this module has any base classes or parent references
grep -n "inherit" source/Classes/AADSyncRuleCount.ps1

Repository: dsccommunity/AADConnectDsc

Length of output: 52


🏁 Script executed:

# Check the psm1 file to see what's imported
head -50 source/AADConnectDsc.psm1 | grep -i "function\|require\|import\|module"

Repository: dsccommunity/AADConnectDsc

Length of output: 152


🏁 Script executed:

# Check if there are other class-based resources in the source/Classes directory
find source/Classes -name "*.ps1" -type f

Repository: dsccommunity/AADConnectDsc

Length of output: 378


🏁 Script executed:

# Check if the module uses DscResource.Base or DscResource.Common in any way
grep -r "DscResource\|ResourceBase" source --include="*.ps1"

Repository: dsccommunity/AADConnectDsc

Length of output: 1172


🏁 Script executed:

# Check if this is actually an issue by looking at similar projects
# First, let's see what other classes exist
ls -la source/Classes/

Repository: dsccommunity/AADConnectDsc

Length of output: 772


🏁 Script executed:

# Check the Prefix.ps1 file to see DscResource.Common loading
cat source/Prefix.ps1 | head -30

Repository: dsccommunity/AADConnectDsc

Length of output: 234


🏁 Script executed:

# Check if the DscResource.Common module exists in the Modules directory
ls -la source/Modules/

Repository: dsccommunity/AADConnectDsc

Length of output: 133


🏁 Script executed:

# Check what exception helpers are available in DscResource.Common
find source/Modules/DscResource.Common -name "*.ps1" -type f | grep -i exception

Repository: dsccommunity/AADConnectDsc

Length of output: 139


🏁 Script executed:

# Let's verify the coding guideline applies to this class specifically
# Check if the class has [DscResource(...)] decoration
grep -A 2 "^class AADSyncRuleCount" source/Classes/AADSyncRuleCount.ps1

Repository: dsccommunity/AADConnectDsc

Length of output: 102


🏁 Script executed:

# Check the context around line 81 to understand the error scenario
sed -n '69,82p' source/Classes/AADSyncRuleCount.ps1 | cat -n

Repository: dsccommunity/AADConnectDsc

Length of output: 879


🏁 Script executed:

# Search for how other DSC resources in this repo handle errors
grep -r "New-InvalidOperationException\|New-InvalidDataException\|throw" source/Classes/*.ps1 | head -20

Repository: dsccommunity/AADConnectDsc

Length of output: 129


Replace raw throw with New-InvalidOperationException.

Line 81 uses throw $message in a class resource with [DscResource()] decoration. Per the coding guidelines, class resources must use New-InvalidOperationException (or another appropriate New-*Exception helper) instead of raw throw for terminating errors.

Since this resource is report-only and cannot remediate drift, New-InvalidOperationException is the appropriate exception type for this scenario.

🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] Missing BOM encoding for non-ASCII encoded file 'AADSyncRuleCount.ps1'

(PSUseBOMForUnicodeEncodedFile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/Classes/AADSyncRuleCount.ps1` around lines 80 - 82, Replace the raw
"throw $message" with a typed exception created by New-InvalidOperationException
to comply with class DSC resource rules: after calling
$this.TryWriteEventLog('Error', 1102, $message) change the termination to "throw
(New-InvalidOperationException -Message $message)" (or equivalent New-*Exception
helper) so the resource (AADSyncRuleCount class/method using
$this.TryWriteEventLog and throw $message) raises a proper
InvalidOperationException rather than a raw throw.


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)"
}
}
}
Comment on lines +1 to +105
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Add unit and integration tests for the new resource before merge.

This PR adds a new DSC resource, but no corresponding resource tests are included in the reviewed changes.

As per coding guidelines, Add unit tests for all commands/functions/resources and Add integration tests for all public commands and resources.

🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] Missing BOM encoding for non-ASCII encoded file 'AADSyncRuleCount.ps1'

(PSUseBOMForUnicodeEncodedFile)


[info] 1-105: No examples found for resource 'AADSyncRuleCount'

(PSDSCDscExamplesPresent)


[info] 1-105: No tests found for resource 'AADSyncRuleCount'

(PSDSCDscTestsPresent)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/Classes/AADSyncRuleCount.ps1` around lines 1 - 105, Add unit and
integration tests for the new DSC resource AADSyncRuleCount: write unit tests
that exercise AADSyncRuleCount::Test, ::Get, ::Set, ::GetScopeDescription and
::TryWriteEventLog using mocked Get-ADSyncRule and Write-AADConnectEventLog to
cover scenarios (all connectors vs specific ConnectorName, zero rules, non-zero
rules, and exception path when writing event log), and add integration tests
that deploy the resource to a test LCM/AD Connect environment to validate
behavior end-to-end (expected count, drift error thrown by Set, and event log
entries with EventId 1100–1102).

Original file line number Diff line number Diff line change
@@ -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
}
}
}