Skip to content

Add multi-forest AD targeting and merged reporting - #2072

Open
bbk007 wants to merge 4 commits into
maester365:mainfrom
bbk007:feature/ad-multi-forest-targeting
Open

Add multi-forest AD targeting and merged reporting#2072
bbk007 wants to merge 4 commits into
maester365:mainfrom
bbk007:feature/ad-multi-forest-targeting

Conversation

@bbk007

@bbk007 bbk007 commented Aug 2, 2026

Copy link
Copy Markdown

Summary

  • add explicit multi-target Active Directory connectivity support via Connect-Maester -ActiveDirectoryServer
  • route AD cmdlets through selected target server across AD checks
  • enrich Invoke-Maester AD run context and include forest context in reporting
  • generate merged base multi-forest report output while keeping per-forest artifacts
  • add local module loader helper for AD test sessions

Validation

  • Invoke-Pester -Path powershell/tests/functions/ActiveDirectoryOptIn.Tests.ps1, powershell/tests/functions/Connect-Maester.Tests.ps1, powershell/tests/functions/Invoke-Maester.Tests.ps1, powershell/tests/functions/Get-MtHtmlReport.Tests.ps1, powershell/tests/functions/Import-MtMaesterResult.Tests.ps1, powershell/tests/functions/Merge-MtMaesterResult.Tests.ps1, powershell/tests/functions/ConvertTo-MtMaesterResult.Tests.ps1 -Output Normal
  • Result: 69 passed, 0 failed

Closes #2071

Summary by CodeRabbit

  • New Features

    • Connect to one or more Active Directory servers or forest targets.
    • Run checks independently for each target with separate results and a combined report.
    • Reports now include forest, domain, and server details.
    • Added validation and clearer handling for unavailable targets.
  • Bug Fixes

    • Improved query targeting, cache isolation, and progress display behavior.
  • Documentation

    • Added local module setup guidance and multi-forest execution examples.
    • Updated the release version to 2.2.0.

@bbk007
bbk007 requested a review from a team as a code owner August 2, 2026 11:08
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 96af2be4-1637-40ce-b8e1-c481b8395f75

📥 Commits

Reviewing files that changed from the base of the PR and between 02e4faa and 7d0703d.

📒 Files selected for processing (1)
  • powershell/public/Invoke-Maester.ps1
🚧 Files skipped from review as they are similar to previous changes (1)
  • powershell/public/Invoke-Maester.ps1

📝 Walkthrough

Walkthrough

The PR adds multi-forest Active Directory targeting. It routes AD queries through selected servers, runs tests per target, records AD context, creates target-specific reports, and merges results.

Changes

Multi-forest Active Directory execution

Layer / File(s) Summary
Module context and result metadata
powershell/Maester.psd1, powershell/Maester.psm1, powershell/internal/*, powershell/tests/functions/*, tests/maester-config.json
The module tracks AD execution context, includes AD metadata in results, updates progress handling, and changes the module version to 2.2.0.
AD target selection and query routing
powershell/public/Connect-Maester.ps1, powershell/public/Get-MtAD*.ps1, powershell/public/ad/*
Connect-Maester accepts multiple servers and records probe results. AD state and directory tests use the selected server.
Multi-target invocation and report generation
powershell/public/Invoke-Maester.ps1
AD-only runs execute independently per target. Results receive target context, output paths receive suffixes, and target results can be merged.
AD runner integration and validation
build/activeDirectory/*, powershell/tests/functions/ActiveDirectoryOptIn.Tests.ps1, powershell/tests/functions/Connect-Maester.Tests.ps1
The runner accepts multiple targets, aggregates reports, documents multi-forest usage, and tests target probing and per-target output generation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant Invoke-Maester
  participant Connect-Maester
  participant ActiveDirectory
  participant ReportWriter
  Operator->>Invoke-Maester: start AD tests with target servers
  Invoke-Maester->>Connect-Maester: connect to current target
  Connect-Maester->>ActiveDirectory: probe RootDSE
  ActiveDirectory-->>Connect-Maester: return AD metadata
  Connect-Maester-->>Invoke-Maester: return connection state
  Invoke-Maester->>ActiveDirectory: execute AD tests
  Invoke-Maester->>ReportWriter: write per-target and merged reports
Loading

Possibly related PRs

Suggested labels: enhancement, maester-core, maester-test

Suggested reviewers: merill

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes webhook redaction and child-notification suppression, which are unrelated to issue [#2071]. Move webhook redaction and child-notification suppression to a separate pull request, or document their required connection to issue #2071.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: multi-forest Active Directory targeting with merged reporting.
Description check ✅ Passed The description explains the implementation, links issue #2071, and reports targeted validation with 69 passing tests.
Linked Issues check ✅ Passed The changes satisfy issue [#2071] by adding multi-forest targeting, AD context reporting, and merged report output.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (3)
powershell/tests/functions/ActiveDirectoryOptIn.Tests.ps1 (1)

107-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the session connection state is restored after the multi-target run.

The test covers per-target execution and the output files. It does not check that Invoke-Maester restores $__MtSession.ADConnection.TargetServer and .TargetServers after the loop. That restore is the state contract for the caller session, and it is currently not protected against a mid-loop failure. See the related comment on powershell/public/Invoke-Maester.ps1 lines 741 to 809.

Add assertions inside InModuleScope Maester after the run.

💚 Proposed addition
         Test-Path (Join-Path $outputFolder 'ADMultiForest.html') | Should -BeTrue
         (Get-ChildItem -Path $outputFolder -Filter 'ADMultiForest-dc01.contoso.com*.json').Count | Should -BeGreaterThan 0
         (Get-ChildItem -Path $outputFolder -Filter 'ADMultiForest-dc01.fabrikam.net*.json').Count | Should -BeGreaterThan 0
+
+        InModuleScope Maester {
+            $__MtSession.ADConnection.TargetServer | Should -Be 'dc01.contoso.com'
+            @($__MtSession.ADConnection.TargetServers).Count | Should -Be 2
+        }
     }
🤖 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 `@powershell/tests/functions/ActiveDirectoryOptIn.Tests.ps1` around lines 107 -
129, Extend the multi-target test in `Runs AD tests once per connected forest
target when multiple targets are configured` to assert after `Invoke-Maester`
completes that `$__MtSession.ADConnection.TargetServer` and `.TargetServers`
match their original values. Place these assertions inside `InModuleScope
Maester` after the run while preserving the existing result and output-file
checks.
powershell/tests/functions/Connect-Maester.Tests.ps1 (1)

82-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reset $__MtSession.ADConnection after these tests.

Both tests write a connected Active Directory state into the module session. No AfterEach clears it. The state persists for the rest of the Pester session, and other test files that import the same module instance can then observe an unexpected connected AD session. ActiveDirectoryOptIn.Tests.ps1 clears this state in its own AfterEach; apply the same pattern here.

♻️ Proposed addition
+    AfterEach {
+        InModuleScope Maester {
+            $__MtSession.ADConnection = $null
+        }
+    }
🤖 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 `@powershell/tests/functions/Connect-Maester.Tests.ps1` around lines 82 - 128,
Add an AfterEach cleanup for the Active Directory connection state created by
the tests around Connect-Maester, resetting $__MtSession.ADConnection in the
Maester module scope after each test. Follow the existing cleanup pattern used
by ActiveDirectoryOptIn.Tests.ps1 and ensure both TargetServer tests leave no
connected session state behind.
powershell/public/Invoke-Maester.ps1 (1)

420-434: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the no-op EndOfJson assignment and rename the merge-source variable.

Line 420 reassigns $mergedResult, which discards the object created at line 369. Only $mergedTenants survives. The reuse of one variable name for two different objects makes the merge flow hard to follow.

Line 434 assigns $mergedResult.EndOfJson back onto $mergedResult. This is a no-op because Select-Object * already copied the property.

♻️ Proposed refactor
-        $mergedResult = Merge-MtMaesterResult -MaesterResults $ForestResults
-        $mergedTenants = $mergedResult.Tenants
+        $tenantMergeResult = Merge-MtMaesterResult -MaesterResults $ForestResults
+        $mergedTenants = $tenantMergeResult.Tenants
         $mergedResult | Add-Member -NotePropertyName 'InvokeCommand' -NotePropertyValue $InvokeCommand -Force
-        $mergedResult | Add-Member -NotePropertyName 'EndOfJson' -NotePropertyValue $mergedResult.EndOfJson -Force
🤖 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 `@powershell/public/Invoke-Maester.ps1` around lines 420 - 434, In the merge
flow around `$firstResult`, rename the source-copy variable currently assigned
with `Select-Object *` to a distinct name so it is not confused with the earlier
`$mergedResult` object; update all subsequent property additions and the emitted
result reference accordingly. Remove the final `Add-Member` statement that
assigns `$mergedResult.EndOfJson` back to the same property.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@build/activeDirectory/Run-ADTests-And-CopyReports.ps1`:
- Around line 213-221: Update the invocation parameters used by Invoke-Maester
so the AD test execution includes every path in $validTestPaths rather than only
index 0, preserving the existing behavior and output for all discovered
locations.

In `@powershell/public/ad/domain/Test-MtAdTombstoneLifetime.ps1`:
- Line 1: Preserve UTF-8 with BOM encoding for the PowerShell scripts containing
emoji literals, retaining PowerShell 5.1 and Desktop compatibility. Apply this
encoding consistently to
powershell/public/ad/domain/Test-MtAdTombstoneLifetime.ps1 (lines 1-1),
powershell/public/ad/gpo/Test-MtAdGpoBlockedInheritanceCount.ps1 (lines 1-1),
powershell/public/ad/passwordpolicy/Test-MtAdAccountLockoutDuration.ps1 (lines
1-1), powershell/public/ad/passwordpolicy/Test-MtAdAccountLockoutThreshold.ps1
(lines 1-1),
powershell/public/ad/passwordpolicy/Test-MtAdFineGrainedPolicyAppliesTo.ps1
(lines 1-1),
powershell/public/ad/passwordpolicy/Test-MtAdFineGrainedPolicyCount.ps1 (lines
1-1), powershell/public/ad/passwordpolicy/Test-MtAdPasswordMinLength.ps1 (lines
1-1), and
powershell/public/ad/passwordpolicy/Test-MtAdPasswordReversibleEncryption.ps1
(lines 1-1); no script logic changes are needed.

In `@powershell/public/ad/group/Test-MtAdGroupEmptyNonPrivilegedCount.ps1`:
- Line 41: Propagate AD lookup failures instead of treating missing results as
valid empty data: update the membership lookups in
Test-MtAdGroupEmptyNonPrivilegedCount.ps1,
Test-MtAdGroupEmptyNonPrivilegedDetails.ps1,
Test-MtAdGroupMemberAccountTypeCount.ps1,
Test-MtAdGroupMemberAccountTypeDetails.ps1,
Test-MtAdGroupMemberDistinctGroupCount.ps1,
Test-MtAdGroupMemberForeignSidCount.ps1,
Test-MtAdGroupMemberForeignSidDetails.ps1, Test-MtAdGroupMemberTrustCount.ps1,
Test-MtAdGroupMemberTrustDetails.ps1, and
Test-MtAdGroupPrivilegedWithMembersCount.ps1 at the specified ranges to use
terminating errors, and update the OU/domain GPO and AD lookups in
Test-MtAdGpoUnlinkedTargetCount.ps1 at both specified ranges similarly. Ensure
failures reach the existing error handling or return an explicit partial/failed
state rather than reporting empty membership or complete GPO coverage.

In `@powershell/public/Invoke-Maester.ps1`:
- Around line 398-433: Update the merged-result construction around
$mergedResult and the $ForestResults aggregation to sum TotalDuration,
UserDuration, DiscoveryDuration, and FrameworkDuration across all forest
results, and set ExecutedAt to the earliest value. Clear or replace
ActiveDirectoryContext so the merged report does not retain one forest’s
context, while preserving the existing counter aggregation and report structure.
- Around line 796-805: Wrap the per-target Invoke-Maester call in the
multi-forest loop with error handling so failures from a target are caught
rather than terminating the run. Record each caught error in the existing result
or reporting flow, then continue iterating through subsequent targets and still
produce the merged report; preserve the current success handling that adds
ADTargetServer, ADForestName, and ADForestRootDomain in the target invocation
block.
- Line 734: Update the Write-MtProgress call’s -Status expression to use
PowerShell subexpression syntax around the full
$__MtSession.ADRunContext.ForestName property path, matching the correct
interpolation pattern on the preceding line.
- Around line 741-809: Wrap the multi-target AD processing loop beginning at
`foreach ($adTarget in $adTargets)` in a `try` block, and move restoration of
`$__MtSession.ADConnection.TargetServer`,
`$__MtSession.ADConnection.TargetServers`, and `$__MtSession.ADRunContext` into
a corresponding `finally` block. Preserve the existing per-target execution and
result aggregation while ensuring restoration runs when `Get-MtAdRunContext`,
`Clear-MtADCache`, or the nested `Invoke-Maester` call throws.

In `@powershell/tests/functions/Get-MtHtmlReport.Tests.ps1`:
- Around line 9-32: Add forest, domain, and target-server AD context to the
fixtures and assertions covering Get-MtHtmlReport, Import-MtMaesterResult, and
Merge-MtMaesterResult. In powershell/tests/functions/Get-MtHtmlReport.Tests.ps1
ranges 9-32 and 52-101, verify single and distinct merged contexts appear in
HTML; in powershell/tests/functions/Import-MtMaesterResult.Tests.ps1 ranges
20-21, 44-45, and 58-59, verify serialization, import, and merged-file expansion
preserve each context; in
powershell/tests/functions/Merge-MtMaesterResult.Tests.ps1 ranges 11-12, 31-32,
85-86, 118-119, and 144-145, cover distinct per-target contexts, three-or-more
results, and empty optional tenant metadata.

---

Nitpick comments:
In `@powershell/public/Invoke-Maester.ps1`:
- Around line 420-434: In the merge flow around `$firstResult`, rename the
source-copy variable currently assigned with `Select-Object *` to a distinct
name so it is not confused with the earlier `$mergedResult` object; update all
subsequent property additions and the emitted result reference accordingly.
Remove the final `Add-Member` statement that assigns `$mergedResult.EndOfJson`
back to the same property.

In `@powershell/tests/functions/ActiveDirectoryOptIn.Tests.ps1`:
- Around line 107-129: Extend the multi-target test in `Runs AD tests once per
connected forest target when multiple targets are configured` to assert after
`Invoke-Maester` completes that `$__MtSession.ADConnection.TargetServer` and
`.TargetServers` match their original values. Place these assertions inside
`InModuleScope Maester` after the run while preserving the existing result and
output-file checks.

In `@powershell/tests/functions/Connect-Maester.Tests.ps1`:
- Around line 82-128: Add an AfterEach cleanup for the Active Directory
connection state created by the tests around Connect-Maester, resetting
$__MtSession.ADConnection in the Maester module scope after each test. Follow
the existing cleanup pattern used by ActiveDirectoryOptIn.Tests.ps1 and ensure
both TargetServer tests leave no connected session state behind.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 319c7d63-2064-430f-83f5-a937e91d64d2

📥 Commits

Reviewing files that changed from the base of the PR and between 5e15463 and 0e6b6c3.

📒 Files selected for processing (47)
  • build/activeDirectory/README-ADTestRunner.md
  • build/activeDirectory/Run-ADTests-And-CopyReports.ps1
  • build/activeDirectory/Use-LocalMaesterModule.ps1
  • powershell/Maester.psd1
  • powershell/Maester.psm1
  • powershell/internal/Clear-ModuleVariable.ps1
  • powershell/internal/ConvertTo-MtMaesterResult.ps1
  • powershell/internal/Reset-MtProgressView.ps1
  • powershell/internal/Set-MtProgressView.ps1
  • powershell/public/Connect-Maester.ps1
  • powershell/public/Get-MtADDomainState.ps1
  • powershell/public/Get-MtADServerParameters.ps1
  • powershell/public/Invoke-Maester.ps1
  • powershell/public/ad/domain/Test-MtAdMachineAccountQuota.ps1
  • powershell/public/ad/domain/Test-MtAdRidsRemaining.ps1
  • powershell/public/ad/domain/Test-MtAdTombstoneLifetime.ps1
  • powershell/public/ad/gpo/Test-MtAdGpoBlockedInheritanceCount.ps1
  • powershell/public/ad/gpo/Test-MtAdGpoLinkedOUCount.ps1
  • powershell/public/ad/gpo/Test-MtAdGpoUnlinkedTargetCount.ps1
  • powershell/public/ad/group/Test-MtAdGroupEmptyNonPrivilegedCount.ps1
  • powershell/public/ad/group/Test-MtAdGroupEmptyNonPrivilegedDetails.ps1
  • powershell/public/ad/group/Test-MtAdGroupMemberAccountTypeCount.ps1
  • powershell/public/ad/group/Test-MtAdGroupMemberAccountTypeDetails.ps1
  • powershell/public/ad/group/Test-MtAdGroupMemberDistinctGroupCount.ps1
  • powershell/public/ad/group/Test-MtAdGroupMemberForeignSidCount.ps1
  • powershell/public/ad/group/Test-MtAdGroupMemberForeignSidDetails.ps1
  • powershell/public/ad/group/Test-MtAdGroupMemberTrustCount.ps1
  • powershell/public/ad/group/Test-MtAdGroupMemberTrustDetails.ps1
  • powershell/public/ad/group/Test-MtAdGroupPrivilegedWithMembersCount.ps1
  • powershell/public/ad/group/Test-MtAdGroupPrivilegedWithMembersDetails.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdAccountLockoutDuration.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdAccountLockoutThreshold.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdFineGrainedPolicyAppliesTo.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdFineGrainedPolicyCount.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdFineGrainedPolicySettingCounts.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdFineGrainedPolicyValueCount.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdPasswordComplexityRequired.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdPasswordHistoryCount.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdPasswordMaxAge.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdPasswordMinLength.ps1
  • powershell/public/ad/passwordpolicy/Test-MtAdPasswordReversibleEncryption.ps1
  • powershell/tests/functions/ActiveDirectoryOptIn.Tests.ps1
  • powershell/tests/functions/Connect-Maester.Tests.ps1
  • powershell/tests/functions/Get-MtHtmlReport.Tests.ps1
  • powershell/tests/functions/Import-MtMaesterResult.Tests.ps1
  • powershell/tests/functions/Merge-MtMaesterResult.Tests.ps1
  • tests/maester-config.json

Comment thread build/activeDirectory/Run-ADTests-And-CopyReports.ps1
Comment thread powershell/public/ad/domain/Test-MtAdTombstoneLifetime.ps1
Comment thread powershell/public/Invoke-Maester.ps1
Comment thread powershell/public/Invoke-Maester.ps1 Outdated
Comment thread powershell/public/Invoke-Maester.ps1 Outdated
Comment thread powershell/public/Invoke-Maester.ps1 Outdated
Comment thread powershell/tests/functions/Get-MtHtmlReport.Tests.ps1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
powershell/public/Invoke-Maester.ps1 (1)

516-528: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Remove OutputFiles from a copy, not from cached tenant results.

Merge-MtMaesterResult assigns Tenants = @($collectedResults) without copying each input result. When report output is written, Write-MtMaesterOutputs uses Select-Object * only for the top result, then removes OutputFiles from the nested tenant PSObject instances. In the multi-forest PassThru path, these tenant objects are the same elements later returned to the caller as $multiForestResults, so the returned results lose OutputFiles after report generation. Clone or deep-copy each tenant result before removing OutputFiles.

🤖 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 `@powershell/public/Invoke-Maester.ps1` around lines 516 - 528, Update the
tenant sanitization in Write-MtMaesterOutputs to clone or deep-copy each tenant
result before removing its OutputFiles property, rather than mutating the
objects held by Tenants and later returned as $multiForestResults. Keep the
top-level copy behavior and output serialization unchanged.
🧹 Nitpick comments (5)
powershell/public/Invoke-Maester.ps1 (4)

413-415: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the discarded first $mergedResult assignment.

Line 413 assigns the output of Merge-MtMaesterResult to $mergedResult. Line 479 overwrites the variable before any other use. Only $mergedTenants from line 414 is used. Assign the merge output to a distinct variable to make the data flow clear.

♻️ Proposed change
-        $mergedResult = Merge-MtMaesterResult -MaesterResults $ForestResults
-        $mergedTenants = $mergedResult.Tenants
+        $mergedTenants = (Merge-MtMaesterResult -MaesterResults $ForestResults).Tenants
🤖 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 `@powershell/public/Invoke-Maester.ps1` around lines 413 - 415, Update the
result-handling block around Merge-MtMaesterResult so the merge output is
assigned to a distinct variable rather than $mergedResult, which is later
overwritten. Preserve assigning $mergedTenants from that merge result and leave
the subsequent first-result handling unchanged.

501-501: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the self-assigning EndOfJson member.

Line 501 reads $mergedResult.EndOfJson and writes the same value back to the same object. The value already comes from $firstResult through the Select-Object * copy at line 479. If $firstResult has no EndOfJson property, this line sets the property to $null, which does not add the marker either. Delete the line, or set the literal marker value.

♻️ Proposed change
         $mergedResult | Add-Member -NotePropertyName 'InvokeCommand' -NotePropertyValue $InvokeCommand -Force
-        $mergedResult | Add-Member -NotePropertyName 'EndOfJson' -NotePropertyValue $mergedResult.EndOfJson -Force
+        $mergedResult | Add-Member -NotePropertyName 'EndOfJson' -NotePropertyValue 'EndOfJson' -Force
🤖 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 `@powershell/public/Invoke-Maester.ps1` at line 501, Remove the self-assigning
EndOfJson Add-Member statement from the merged result construction; rely on the
existing Select-Object * copy from $firstResult, or assign the intended literal
marker value if the property must always be present.

340-346: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Escape single quotes in reconstructed values.

Lines 341 and 343 wrap values in single quotes without escaping. A value that contains ' produces a command string that cannot be pasted back into a shell. Double the quote character when you build the string.

♻️ Proposed change
             } elseif ($paramValue -is [array]) {
-                $invokeCommand += " -$paramName @('$($paramValue -join "', '")')"
+                $escapedValues = @($paramValue | ForEach-Object { "$_".Replace("'", "''") })
+                $invokeCommand += " -$paramName @('$($escapedValues -join "', '")')"
             } elseif ($paramValue -is [string]) {
-                $invokeCommand += " -$paramName '$paramValue'"
+                $invokeCommand += " -$paramName '$($paramValue.Replace("'", "''"))'"
🤖 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 `@powershell/public/Invoke-Maester.ps1` around lines 340 - 346, Update the
array and string branches in the command reconstruction logic to escape embedded
single quotes by doubling them before wrapping values in single quotes. Apply
this to the values handled in the $paramValue array and string checks, while
preserving existing formatting for values without quotes and other parameter
types.

953-970: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the -Comment parameter of the helper.

Get-MtInvokeMaesterCommand accepts -Comment and appends the " # " prefix at line 350. Lines 967 to 969 repeat that concatenation. Build $adContextParts first, then call the helper once with -Comment.

♻️ Proposed change
-        $invokeMaesterCommand = Get-MtInvokeMaesterCommand -BoundParameters $PSBoundParameters
-
+        $adContextParts = @()
         if ($__MtSession.ADRunContext) {
-            $adContextParts = @()
             if (-not [string]::IsNullOrWhiteSpace($__MtSession.ADRunContext.ForestName)) {
@@
-
-            if ($adContextParts.Count -gt 0) {
-                $invokeMaesterCommand += " # " + ($adContextParts -join '; ')
-            }
         }
+
+        $invokeMaesterCommand = Get-MtInvokeMaesterCommand -BoundParameters $PSBoundParameters -Comment ($adContextParts -join '; ')
🤖 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 `@powershell/public/Invoke-Maester.ps1` around lines 953 - 970, The AD context
comment is appended manually after Get-MtInvokeMaesterCommand, duplicating the
helper’s comment formatting. In the Invoke-Maester flow, build the
$adContextParts content first, then pass the joined value through
Get-MtInvokeMaesterCommand’s -Comment parameter and remove the direct " # "
concatenation, while preserving the existing behavior when no AD context parts
exist.
powershell/tests/functions/Get-MtHtmlReport.Tests.ps1 (1)

147-153: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the root-domain assertion distinct.

Line 152 checks *contoso.com*. Lines 150 and 151 already guarantee that substring, because forest-single.contoso.com and dc01.contoso.com both contain it. The assertion cannot fail on its own, so ForestRootDomain is not covered. Use a distinct root-domain value in the fixture, or assert the JSON property name together with the value.

🤖 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 `@powershell/tests/functions/Get-MtHtmlReport.Tests.ps1` around lines 147 -
153, Update the AD context metadata test around “Should contain AD context
metadata in the output” so the ForestRootDomain assertion cannot pass solely
because forest-single.contoso.com or dc01.contoso.com contains contoso.com. Use
a distinct root-domain fixture value or assert the ForestRootDomain property
name together with its value, while preserving the existing forest and
domain-controller checks.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@powershell/public/Invoke-Maester.ps1`:
- Around line 846-861: Update the target output-path construction in the
recursive target handling to always assign targetParams['OutputFolderFileName']
using baseFileName and targetSuffix, including when no output parameters are
provided. Remove the conditional behavior that routes the default configuration
through ValidateAndSetOutputFiles, while preserving explicit output-file suffix
handling where applicable.
- Around line 895-899: Update the merged-report flow around
New-MtMergedAdForestResult to avoid calling it when multiForestResults is empty,
while preserving the existing merge and ADTargetFailures behavior when results
exist. Use a collection-count guard before invoking the function, or adjust its
parameter contract and handle the all-failure path there.
- Around line 383-411: Update ConvertTo-MtTimeSpan and ConvertTo-MtDateTime to
use the TryParse overloads that accept
[System.Globalization.CultureInfo]::InvariantCulture, preserving their existing
fallback values of [TimeSpan]::Zero and $null when parsing fails.

---

Outside diff comments:
In `@powershell/public/Invoke-Maester.ps1`:
- Around line 516-528: Update the tenant sanitization in Write-MtMaesterOutputs
to clone or deep-copy each tenant result before removing its OutputFiles
property, rather than mutating the objects held by Tenants and later returned as
$multiForestResults. Keep the top-level copy behavior and output serialization
unchanged.

---

Nitpick comments:
In `@powershell/public/Invoke-Maester.ps1`:
- Around line 413-415: Update the result-handling block around
Merge-MtMaesterResult so the merge output is assigned to a distinct variable
rather than $mergedResult, which is later overwritten. Preserve assigning
$mergedTenants from that merge result and leave the subsequent first-result
handling unchanged.
- Line 501: Remove the self-assigning EndOfJson Add-Member statement from the
merged result construction; rely on the existing Select-Object * copy from
$firstResult, or assign the intended literal marker value if the property must
always be present.
- Around line 340-346: Update the array and string branches in the command
reconstruction logic to escape embedded single quotes by doubling them before
wrapping values in single quotes. Apply this to the values handled in the
$paramValue array and string checks, while preserving existing formatting for
values without quotes and other parameter types.
- Around line 953-970: The AD context comment is appended manually after
Get-MtInvokeMaesterCommand, duplicating the helper’s comment formatting. In the
Invoke-Maester flow, build the $adContextParts content first, then pass the
joined value through Get-MtInvokeMaesterCommand’s -Comment parameter and remove
the direct " # " concatenation, while preserving the existing behavior when no
AD context parts exist.

In `@powershell/tests/functions/Get-MtHtmlReport.Tests.ps1`:
- Around line 147-153: Update the AD context metadata test around “Should
contain AD context metadata in the output” so the ForestRootDomain assertion
cannot pass solely because forest-single.contoso.com or dc01.contoso.com
contains contoso.com. Use a distinct root-domain fixture value or assert the
ForestRootDomain property name together with its value, while preserving the
existing forest and domain-controller checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c9e3f71-8555-46a9-9e2a-d28a2e82bdf9

📥 Commits

Reviewing files that changed from the base of the PR and between 0e6b6c3 and a0b0768.

📒 Files selected for processing (5)
  • build/activeDirectory/Run-ADTests-And-CopyReports.ps1
  • powershell/public/Invoke-Maester.ps1
  • powershell/tests/functions/Get-MtHtmlReport.Tests.ps1
  • powershell/tests/functions/Import-MtMaesterResult.Tests.ps1
  • powershell/tests/functions/Merge-MtMaesterResult.Tests.ps1
🚧 Files skipped from review as they are similar to previous changes (1)
  • build/activeDirectory/Run-ADTests-And-CopyReports.ps1

Comment thread powershell/public/Invoke-Maester.ps1
Comment thread powershell/public/Invoke-Maester.ps1
Comment thread powershell/public/Invoke-Maester.ps1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
powershell/public/Invoke-Maester.ps1 (2)

323-345: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact secret-bearing parameters from InvokeCommand.

TeamChannelWebhookUri is added to the reconstructed command as plaintext. Line 500 stores that command on the merged result, and Line 533 serializes it to JSON. This can disclose the webhook URL and its query credentials to report readers.

Redact TeamChannelWebhookUri before appending it to the command.

Proposed fix
         $invokeCommand = 'Invoke-Maester'
+        $redactedParameters = @('TeamChannelWebhookUri')
         foreach ($param in $BoundParameters.GetEnumerator()) {
             $paramName = $param.Key
             $paramValue = $param.Value
-            if ($paramValue -is [switch]) {
+            if ($paramName -in $redactedParameters) {
+                $invokeCommand += " -$paramName '<redacted>'"
+            } elseif ($paramValue -is [switch]) {
🤖 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 `@powershell/public/Invoke-Maester.ps1` around lines 323 - 345, The
Get-MtInvokeMaesterCommand function currently appends TeamChannelWebhookUri as
plaintext; redact this parameter before adding it to $invokeCommand. Preserve
the existing command reconstruction for all other parameters while ensuring the
stored and serialized InvokeCommand cannot expose the webhook URL or query
credentials.

841-871: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Send notifications only for the merged result.

When notification parameters are supplied, each recursive target call receives them in $targetParams. Each child then sends email or Teams output at Lines 572-585. The parent sends the merged result again after the loop. A multi-forest run therefore sends one notification per target plus one merged notification.

Remove notification parameters from $targetParams. Send the merged notification once from the parent call.

Proposed fix
                     $targetParams['PassThru'] = $true
+                    $targetParams.Remove('MailRecipient')
+                    $targetParams.Remove('TeamId')
+                    $targetParams.Remove('TeamChannelId')
+                    $targetParams.Remove('TeamChannelWebhookUri')
 
                     try {
🤖 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 `@powershell/public/Invoke-Maester.ps1` around lines 841 - 871, Update the
recursive target-parameter construction around $targetParams so notification
parameters are excluded before invoking Invoke-Maester for each target. Preserve
all other target parameters and keep notification handling in the parent flow,
sending only the merged result once after the target loop.
🤖 Prompt for all review comments with 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.

Outside diff comments:
In `@powershell/public/Invoke-Maester.ps1`:
- Around line 323-345: The Get-MtInvokeMaesterCommand function currently appends
TeamChannelWebhookUri as plaintext; redact this parameter before adding it to
$invokeCommand. Preserve the existing command reconstruction for all other
parameters while ensuring the stored and serialized InvokeCommand cannot expose
the webhook URL or query credentials.
- Around line 841-871: Update the recursive target-parameter construction around
$targetParams so notification parameters are excluded before invoking
Invoke-Maester for each target. Preserve all other target parameters and keep
notification handling in the parent flow, sending only the merged result once
after the target loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6579d0fb-6c48-4645-920a-b186c6d6ad35

📥 Commits

Reviewing files that changed from the base of the PR and between a0b0768 and 02e4faa.

📒 Files selected for processing (1)
  • powershell/public/Invoke-Maester.ps1

@bbk007

bbk007 commented Aug 3, 2026

Copy link
Copy Markdown
Author

PR updated with follow-up security/notification hardening in commit 7d0703d:\n\n- Redacted TeamChannelWebhookUri in InvokeCommand reconstruction so webhook/query credentials are not persisted.\n- Removed notification parameters from recursive per-target Invoke-Maester calls to keep notifications parent-only for merged multi-forest runs.\n\nValidation: Invoke-Pester on powershell/tests/functions/Invoke-Maester.Tests.ps1 and powershell/tests/functions/ActiveDirectoryOptIn.Tests.ps1 (13 passed, 0 failed).

@soulemike

Copy link
Copy Markdown
Contributor

@bbk007 Thanks for your contributions! This looks like a lot of work and it is appreciated.

Do you mind walking through the approach for the wrapper function?

I can see there may be some logic where this makes sense but I don't want to make assumptions. It is enough of a departure from the current approach that wouldn't mind some discussion just to capture the rational as we work through these new capabilities.

@bbk007

bbk007 commented Aug 6, 2026

Copy link
Copy Markdown
Author

@bbk007 Thanks for your contributions! This looks like a lot of work and it is appreciated.

Do you mind walking through the approach for the wrapper function?

I can see there may be some logic where this makes sense but I don't want to make assumptions. It is enough of a departure from the current approach that wouldn't mind some discussion just to capture the rational as we work through these new capabilities.

Hello @soulemike
Assuming you are referring the Get-MtADServerParameters.ps1?
It could help pass any additional parameters in future like -credential, Today all the script are running with logged in user or system (if ran it through a pipeline) context and those may not have necessary privileges to run all tests and -credential could be used to pass a high privileged user credential to run those scripts. Hope it helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhancement: Multi-forest Active Directory targeting with merged report output

2 participants