Skip to content

v1.2.0 — BitLocker, Windows LAPS, and deprecated-settings checks for Intune Anomalies#7

Merged
royklo merged 14 commits into
mainfrom
feature/intune-anomalies-bitlocker-laps
Jun 17, 2026
Merged

v1.2.0 — BitLocker, Windows LAPS, and deprecated-settings checks for Intune Anomalies#7
royklo merged 14 commits into
mainfrom
feature/intune-anomalies-bitlocker-laps

Conversation

@royklo

@royklo royklo commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

Adds three new checks to Get-IntuneAnomaliesReport, significantly speeds the report up via Graph $batch, and ships a backwards-compatible bump to 1.2.0.

New detections

  • BitLocker key escrow — flags devices where Intune assigned a BitLocker policy but no OS-volume recovery key is in Entra, plus devices that aren't encrypted at all. Resolves
    assignments across Settings Catalog, legacy device configurations, and Endpoint Security intents, honouring include / exclude groups and assignment filters.
  • Windows LAPS backup — flags devices covered by an Entra-backed LAPS policy with no local admin credential backed up, or whose most recent backup is older than 60 days.
  • Deprecated Settings Catalog settings — walks every Settings Catalog policy and flags settings Microsoft has marked deprecated. Catalog data is cached.
  • New -ShowExcludedDevices switch surfaces devices that were deliberately excluded from BitLocker / LAPS policies as Info-severity rows. Off by default.

Performance

End-to-end report runtime on a 75-policy / 2-device tenant dropped from ~19s to ~8s. Larger tenants see proportionally bigger wins.

The main lever is a new private Graph $batch helper used across five hot paths (compliance fetch, Settings Catalog /settings, app ?$expand=assignments, group
transitiveMembers, and the prior $expand=settings paginated fetch). Two quadratic per-row lookups in the BitLocker/LAPS and app-failure paths were also replaced with
hashtable indexes.

Permissions

Two additional Microsoft Graph scopes are required to run the BitLocker / LAPS checks:

  • BitlockerKey.ReadBasic.All
  • DeviceLocalCredential.ReadBasic.All

Both are ReadBasic variants — the report confirms a recovery key or password backup exists, but never reads the actual recovery password or local admin password.
Connect-RKGraph auto-detects missing scopes on existing tokens and re-grants consent for you.

Fixes

  • DataTables didn't always initialise correctly in the rendered report.
  • Two quadratic lookups that slowed the report at higher device counts.

Polish

  • BitLocker and LAPS tabs drop Manufacturer, Model, and Policy Assigned columns (irrelevant to the security finding each row describes). Inventory-focused tabs keep them.
  • Every stat tile in the report now uses a distinct color.

Test plan

  • Invoke-Pester ./Tests/Consistency.Tests.ps1 — 11 / 11 tests pass (6 new: report-cmdlet shape contracts, BitLocker / LAPS resolver column contracts, scope-drift check
    between Connect-RKGraph defaults and docs/PERMISSIONS.md)
  • Import-Module ./module/RKSolutions.psd1 -Force — clean import; manifest reports 1.2.0
  • Invoke-ScriptAnalyzer -Path ./module -Severity Warning,Error — no new findings introduced by this branch
  • Get-IntuneAnomaliesReport -Verbose against a real tenant — the new BitLocker, LAPS, and Deprecated Settings tabs render, all stat tiles show distinct colors
  • Get-IntuneAnomaliesReport -ShowExcludedDevices — Info-severity exclusion rows appear for devices deliberately excluded from BitLocker / LAPS policies
  • First-time connect with an existing session: Connect-RKGraph should detect the two new *.ReadBasic.All scopes and walk you through the consent flow

Roy Klooster and others added 13 commits June 17, 2026 20:21
…ow + deprecated settings, consent repair

- Add Invoke-RKGraphBatch private helper (Graph $batch wrapper with chunking, 429/5xx retry, id correlation)
- Refactor Get-AllDeviceData compliance fetch into two batched prefetch passes
- Add BitLocker key escrow + Windows LAPS backup anomaly detection (assignment-aware, filter-aware, exclude-group-aware)
- Add Deprecated Settings detection with cached Get-RKIntuneSettingsCatalog helper
- Fold "Not Encrypted" tab into BitLocker tab (unencrypted devices surface there)
- Add Connect-RKGraph auto-repair flow for missing scopes (Invoke-RKConsentGrantRepair + Clear-RKMsalTokenCache)
- Inject template-wide search bar into .rk-filter-bar via initRKTable
- Add verbose timing instrumentation across Get-IntuneAnomaliesReport and Get-AllDeviceData
- Fix broken JS in rendered template (missing backtick escape in select.append here-string)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…licy discovery

Replace configurationPolicies?$expand=settings (Graph hard-caps page size with
$expand, observed ~1-2 policies per cosmos page) with:

  1. Bare list: GET configurationPolicies?$top=999 (small payload, 1-3 pages)
  2. Per-policy GET /settings via Invoke-RKGraphBatch (sub-requests parallelize
     server-side, ~75 policies finish in 4 batches of 20)

Measured on a 75-policy / 2-device tenant: configurationPolicies discovery
dropped from ~9.9s to ~1.3s; the full BitLocker/LAPS context phase from
~13.0s to ~4.2s; total Get-IntuneAnomaliesReport from 17.8s to 9.2s (-48%).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolve-IntuneLapsAnomalies's secondary join enumerated all LAPS credentials
for every device whose primary azureAdDeviceId lookup failed. At 1000 devices
x 1000 LAPS entries that's up to 1M iterations per report.

Build LapsCredentialByDeviceName at index time and use a hashtable lookup
instead. Same data, same join semantics, same caller-visible behavior - just
not quadratic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… app lookup

Get-ApplicationFailures had two scaling bugs:
1. Per-row $apps | Where-Object scans (twice per failed row).
2. One sequential ?$expand=assignments GET per failed app.

Pre-build an id->app hashtable for O(1) joins, collect the unique application
ids that need an expand, and issue them as a single $batch (chunked to 20 by
the helper). Behaviorally identical; same assignment-status logic, same shape.

Projected wall-clock saving at 50 failing apps: ~10s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…/LAPS context

Group device-member + user-member resolution previously issued 2 sequential
v1.0/groups/{id}/transitiveMembers GETs per referenced group. At a tenant with
20 BL/LAPS-targeted groups that's 40 sequential round trips.

Pre-allocate the membership HashSets so a failed sub-request can never leave
a referenced group missing, batch all (group, devices|users) requests via
Invoke-RKGraphBatch, then fall back to sequential pagination only for groups
whose first page returned an @odata.nextLink (rare: >999 members).

Behavior preserved on small tenants (verified locally); scaling win lands on
tenants with many BL/LAPS-targeted groups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…at tile a unique color

BitLocker and LAPS tabs drop Manufacturer, Model, and Policy Assigned columns:
- Manufacturer/Model are device-inventory data, irrelevant to the security
  anomaly each row describes (other tabs that ARE device-inventory keep them).
- Policy Assigned is redundant with Applied Policies - if anything is shown
  there, the device is in scope.

Stat tiles: rotate BITLOCKER -> t-steel (reuses already-defined unused token),
LAPS BACKUP -> t-indigo (new), DEPRECATED SETTINGS -> t-pink (new). All ten
tiles now use distinct colors. Light/dark theme parity preserved for the two
new tokens (background, border, eyebrow, caption all defined).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…voke-RKSolutionsWithConnection)

Get-GroupActivationDetails in EntraAdminRoles.ps1 was defined but never called.
Invoke-RKSolutionsWithConnection (whole file + load-order entry) was a wrapper
that no public cmdlet uses - report cmdlets call Connect-ToMgGraph directly via
Connect-RKGraph today.

~115 lines removed. Module imports unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…APS work

Captures the non-obvious patterns the codebase can't tell future-readers on its
own: the Graph \$expand page-size cap, the \$batch helper's pagination caveat,
the two O(N^2) accidental traps that bit this session, the module load-order
constraint between IntuneAnomalies and IntuneEnrollmentFlows, and the
\$script:AllGroups cache dependency between the two reports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lver columns

Six new tests, all CI-safe (no Microsoft Graph connection required):

  - Public report cmdlet shape (3 tests):
      * Every report has -ExportPath or -OutputPath (caller can direct output).
      * Every report uses [CmdletBinding()] so -Verbose flows.
      * Every report file source-grep'd for Get-MgContext (connection guard).

  - BitLocker / LAPS anomaly resolver column contracts (2 tests):
      Calls Resolve-IntuneBitLockerAnomalies / Resolve-IntuneLapsAnomalies
      with synthetic in-memory context + device, asserts the exact column
      set + order. Would have caught today's column-trim regression
      (Manufacturer/Model/PolicyAssigned removal) at the data layer instead
      of by eyeballing the rendered HTML.

  - Connect-RKGraph default scopes ↔ docs/PERMISSIONS.md (1 test):
      Scope drift detector. Every scope literal in Connect-RKGraph.ps1
      must appear in docs/PERMISSIONS.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CHANGELOG.md: expand the [Unreleased] section with the perf wins (measured
  19.0s -> 8.2s on dev tenant), the column-trim + unique-tile-colors UI work,
  the O(N*M) fixes, dead-code removal, and the new smoke tests. Document the
  new -ShowExcludedDevices switch and the Connect-RKGraph auto-repair flow.

- docs/CMDLET-REFERENCE.md: Get-IntuneAnomaliesReport now documents
  -ShowExcludedDevices and its description reflects the actual report scope
  (BitLocker / LAPS / deprecated settings, not just "anomalies").

- FINDINGS.md moved from repo root to .claude/FINDINGS.md to match the
  InforcerCommunity precedent. Local-only since .claude/ is gitignored.
  Skill references updated to the new path.

- docs/PERMISSIONS.md and README.md: no change needed - already accurate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to the previous commit (which only captured the FINDINGS.md move):

- CHANGELOG.md: add Performance, Fixes, UI, and expanded Maintenance subsections
  for the perf wins (19.0s -> 8.2s on a 75-policy / 2-device dev tenant), the
  O(N*M) lookup fixes, the column-trim + unique tile colors UI work, dead code
  removal, and the new Pester smoke tests. Also document the new switch and
  the auto-repair flow under Features.

- docs/CMDLET-REFERENCE.md: Get-IntuneAnomaliesReport now documents
  -ShowExcludedDevices and its description reflects what the report actually
  covers (BitLocker, LAPS, deprecated Settings Catalog, app failures, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- module/RKSolutions.psd1: 1.1.0 -> 1.2.0 (minor: new features + perf, no
  breaking changes to public cmdlet surface).
- CHANGELOG.md: [Unreleased] cut into [1.2.0] - 2026-06-17, condensed for an
  Intune/Entra admin audience (mid-level technical, not symbol-level).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 17, 2026 19:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR bumps RKSolutions to v1.2.0 and expands Get-IntuneAnomaliesReport with new security/configuration detections (BitLocker key escrow, Windows LAPS backup, and deprecated Settings Catalog settings), while improving runtime by introducing a reusable Microsoft Graph $batch helper and replacing several quadratic lookups with indexed structures.

Changes:

  • Add BitLocker + Windows LAPS anomaly discovery (assignment resolution + Entra escrow/backup verification) and deprecated Settings Catalog detection, including -ShowExcludedDevices.
  • Introduce Invoke-RKGraphBatch and apply batching to multiple hot paths (compliance detail fetch, Settings Catalog settings fetch, app assignments expansion, group transitive members).
  • Update report UI/template (new tabs, new stat tiles/colors, custom per-table search input) and add Pester shape/contract tests + permission docs updates.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Tests/Consistency.Tests.ps1 Adds AST/shape contracts for public report cmdlets and output-column contracts for BitLocker/LAPS resolvers; adds doc drift check for default scopes.
module/RKSolutions.psm1 Updates private script load order; adds new helpers (Invoke-RKGraphBatch, Get-RKIntuneSettingsCatalog).
module/RKSolutions.psd1 Bumps module version to 1.2.0 and enforces PowerShell 7.0+.
module/Public/Get-IntuneAnomaliesReport.ps1 Wires in new detections + -ShowExcludedDevices; adds phase timing verbose telemetry; removes standalone “Not Encrypted” tab generation.
module/Public/Connect-RKGraph.ps1 Adds two new default Graph scopes + -SkipAutoRepair; ensures default RequiredScopes are forwarded to the underlying connector.
module/Private/Invoke-RKSolutionsWithConnection.ps1 Removed legacy connection wrapper function.
module/Private/Invoke-RKGraphBatch.ps1 New private Graph $batch helper with retry logic and correlation by request id.
module/Private/IntuneAnomalies.ps1 Major changes: new HTML panels/tables/filters for BitLocker/LAPS/Deprecated Settings; new batching in device compliance resolution + app failures; adds BitLocker/LAPS/deprecation resolver implementations.
module/Private/Get-RKSolutionsReportTemplate.ps1 Updates DataTables layout and injects a custom per-table search input; adds new stat tile color variants.
module/Private/Get-RKIntuneSettingsCatalog.ps1 New cached downloader/loader for Settings Catalog definition metadata from GitHub releases.
module/Private/EntraAdminRoles.ps1 Removes unused Get-GroupActivationDetails helper.
module/Private/Connect-ToMgGraph.ps1 Adds scope auto-repair flow (consent grant patch + token cache wipe) and -SkipAutoRepair.
docs/PERMISSIONS.md Documents new required scopes and updates the default scope union list.
docs/CMDLET-REFERENCE.md Updates Get-IntuneAnomaliesReport documentation (new tabs + parameters + examples).
CHANGELOG.md Adds v1.2.0 release notes for new detections, batching, permissions, and UI polish.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread module/Private/IntuneAnomalies.ps1 Outdated
Comment thread module/Private/IntuneAnomalies.ps1 Outdated
Comment thread module/Private/IntuneAnomalies.ps1 Outdated
Comment on lines +187 to +190
$actualColumns = @($row.PSObject.Properties.Name)
$expectedColumns = @('Customer','DeviceName','PrimaryUser','Serialnumber','IsEncrypted','AppliedPolicies','KeyEscrowed','Status','Severity')
$actualColumns | Should -Be $expectedColumns -Because 'BitLocker anomaly rows must match the HTML template <td> order'
}
Comment on lines +238 to +241
$actualColumns = @($row.PSObject.Properties.Name)
$expectedColumns = @('Customer','DeviceName','PrimaryUser','Serialnumber','OwnerType','AppliedPolicies','LastBackupDateTime','BackupAgeDays','Status','Severity')
$actualColumns | Should -Be $expectedColumns -Because 'LAPS anomaly rows must match the HTML template <td> order'
}
Comment on lines +54 to +57
$ProgressPreference = 'SilentlyContinue'
$tmpFile = "$cacheFile.tmp"
Invoke-WebRequest -Uri $sourceUrl -OutFile $tmpFile -UseBasicParsing -ErrorAction Stop
Move-Item -Path $tmpFile -Destination $cacheFile -Force
…al devices

Resolve-IntuneLapsAnomalies read \$d.OwnerType but Get-AllDeviceData emits
DeviceOwnership. Every real device hit the LAPS tab with an empty Ownership
cell. The initial smoke test passed because it synthesized OwnerType directly
on the fake device - the test was masking the bug instead of catching it.

Fix:
- Read \$d.DeviceOwnership in Resolve-IntuneLapsAnomalies.
- Switch all three resolver output literals to [PSCustomObject][ordered]@{}
  so property iteration order is deterministic. While here, align literal
  order with the HTML <td> column order (Severity before Status) - the
  report rendered correctly via property-name access but Export-Csv on the
  same objects would have emitted them in the wrong order.
- Update test fixture to use the real Get-AllDeviceData field name
  (DeviceOwnership) AND assert the materialised value, not just the column
  presence, so a future field-name drift can't sneak past the test the way
  it did this time.

Fixes copilot findings #1-#5 on the PR. Finding #6 (-UseBasicParsing breaks
PS 7+) is rejected as empirically wrong: the parameter is a documented no-op
on PS 6+, verified working on PS 7.5.4, and the settings catalog has been
downloading successfully all session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@royklo royklo merged commit f05b1f2 into main Jun 17, 2026
2 checks passed
@royklo royklo deleted the feature/intune-anomalies-bitlocker-laps branch June 17, 2026 19:51
@royklo royklo mentioned this pull request Jun 18, 2026
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.

2 participants