Skip to content

release: v0.6.0 — Get-InforcerSecureScore + audit event filter + dynamic completer#38

Merged
royklo merged 22 commits into
mainfrom
release/v0.6.0
Jul 7, 2026
Merged

release: v0.6.0 — Get-InforcerSecureScore + audit event filter + dynamic completer#38
royklo merged 22 commits into
mainfrom
release/v0.6.0

Conversation

@royklo

@royklo royklo commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Version bump 0.5.0 → 0.6.0.

What's new

🆕 Get-InforcerSecureScore

Retrieves the Microsoft Secure Score for a tenant, including:

  • Current score, max score, and percentage
  • Up to 90 days of daily score history
  • Per-category breakdown (Identity, Data, Device, etc.)
  • Actionable control profiles with remediation guidance and score-gap ranking
# Summary
Get-InforcerSecureScore -TenantId 139

# Top 10 remediation opportunities (biggest score gap first)
(Get-InforcerSecureScore -TenantId 139).ControlProfiles |
    Sort-Object ScoreDifference -Descending |
    Select-Object Title, Service, CurrentScore, MaxScore, Remediation -First 10

# 90-day trend
(Get-InforcerSecureScore -TenantId 139).Scores

Accepts numeric tenant ID, GUID, or friendly name. Supports -OutputType JsonObject. Requires Tenants.SecureScores.Read (+ Tenants.Read when using GUID/name).

🔎 Get-InforcerAuditEvent -User <string>

New parameter for server-side filtering. Previously you had to pull everything and filter locally:

# Before: fetch broad, filter local
Get-InforcerAuditEvent -MaxResults 1000 | Where-Object User -eq 'admin@contoso.com'

# Now: server-side, faster, no wasted pagination
Get-InforcerAuditEvent -User 'admin@contoso.com' -DateFrom (Get-Date).AddDays(-30)

💡 -EventType tab completion is now dynamic

Tab-completing -EventType used to be limited to a hardcoded list bundled with the module. It now reflects whatever event types the API currently supports — as soon as you run
Get-InforcerSupportedEventType in a session, tab completion is up to date. New Inforcer event types no longer require a module update to be tab-completable.

📖 Get-Help now shows required scopes

Get-Help Get-InforcerX now surfaces the required API scope inline for 13 more cmdlets. No more flipping between PowerShell and the docs to figure out permissions:


NAME
    Get-InforcerTenant
    
SYNOPSIS
    Retrieves tenant information from the Inforcer API.
    
    Required API scope(s): Tenants.Read

17 of 20 cmdlets now show scope inline. The three that don't (Connect-Inforcer, Disconnect-Inforcer, Test-InforcerConnection) don't have a data-scope call.

Bug fixes

  • Get-InforcerUser -UserId returned the wrong shape. The cmdlet was returning one of the user's nested arrays (groups, licenses, etc.) instead of the user object itself.
    PSTypeName never applied, aliases silently no-op'd. Fixed — Get-InforcerUser -TenantId X -UserId Y now correctly returns a single InforcerCommunity.User object.
  • Get-InforcerSecureScore nested aliasing extended to .ControlCategoryScores and its inner .historicScores array so category scores get PascalCase aliases like the
    rest of the object.
  • Removed a stale PolicyDiffFormatted mention in the tenant cmdlet documentation (the property was removed in an earlier release).

Full cmdlet list

# Cmdlet Change in this release
21 Get-InforcerSecureScore ✨ new
20 Get-InforcerAuditEvent +-User param, Id alias, dynamic completer
19 Get-InforcerUser 🐛 fixed -UserId returning wrong shape
13 other cmdlets 📖 Get-Help now shows required scope

Testing

  • Pester: 162/162 pass (was 159 — added 3 for SecureScore alias + AuditEvent Id + not-connected cases)
  • Test-ModuleManifest: valid, v0.6.0
  • Live API verification against a real tenant: every new/changed capability confirmed working (single-object shapes, alias population, server-side filter honored, dynamic
    completer refreshes correctly)

Test plan for reviewer

  • Merge PR
  • Pull main locally, Import-Module ./module -Force
  • Get-Command -Module InforcerCommunity | Measure-Object → 21
  • Get-Help Get-InforcerSecureScore shows the new cmdlet
  • Optional: publish to PowerShell Gallery via the standard workflow

royklo added 5 commits July 6, 2026 22:47
…ffFormatted mention

- Adds `Required API scope(s):` line to the .SYNOPSIS block of 13 cmdlets
  that previously lacked it, matching the pattern already used by the
  Reports cmdlets (Get-InforcerReportRun, Get-InforcerReportType,
  Invoke-InforcerReport, Save-InforcerReportOutput). `Get-Help <cmdlet>`
  now surfaces the scope requirement without users needing to open
  docs/API-REFERENCE.md.
- Removes the stale `PolicyDiffFormatted` reference in
  docs/CMDLET-REFERENCE.md that survived when the property itself was
  deleted from the module (FINDINGS #63).
- 3 cmdlets intentionally excluded from scope lines: Connect-Inforcer,
  Disconnect-Inforcer, Test-InforcerConnection (no scope-gated API call).

Scope values sourced from the existing Cmdlet → Endpoints → Required
Scopes table in docs/API-REFERENCE.md.

159/159 Pester tests pass.
… alias

New cmdlet Get-InforcerSecureScore
- Wraps GET /beta/tenants/{tenantId}/secureScores — 90-day score history,
  per-category breakdown, and actionable control profiles with remediation.
- Accepts numeric ID, GUID, or tenant name via -TenantId.
- Supports -OutputType JsonObject.
- PSTypeName InforcerCommunity.SecureScore with a ListControl view showing
  current/max/percentage plus counts of nested arrays.
- Aliases: CurrentScore, CurrentScorePercentage, MaxScore, LicensedUserCount,
  EnabledServices, Scores, ControlProfiles, ControlCategoryScores. Nested
  arrays (Scores, ControlProfiles) get their own PascalCase aliases.

Get-InforcerAuditEvent -User
- New parameter that server-side filters audit events by the specified user.
- Sends the value as the `user` field in the POST /beta/auditEvents/search
  body. Previously users had to fetch all events and filter client-side.

AuditEvent.Id alias
- AuditEvent objects now expose the raw `id` field as PascalCase `Id`,
  consistent with every other object type in the module.

Docs / plumbing
- README, CMDLET-REFERENCE, and API-REFERENCE updated (endpoint section,
  three new schemas, cmdlet-to-scope table row, TOC).
- CHANGELOG entry for 0.6.0.
- Module version bumped 0.5.0 → 0.6.0.
- Consistency tests: expected count 20 → 21, new not-connected test, new
  SecureScore and AuditEvent alias tests, updated Get-InforcerAuditEvent
  param list.
- scripts/Test-AllCmdlets.ps1: new case for Get-InforcerSecureScore.

162/162 Pester tests pass.
…ication

Two `Invoke-InforcerApiRequest` callers were missing `-PreserveStructure`, causing
the helper's "if .data is a single object with an array property, unwrap to that
array" convenience to return the wrong shape for single-object endpoints:

- `Get-InforcerSecureScore`: was returning the 90-item `scores` array instead of
  the top-level `tenantSecureScoreDetails` object. PSTypeName was never applied.
  Live-verified fix: 90/90 assertions pass, aliases populated on the top-level
  object plus all three nested arrays.

- `Get-InforcerUser -UserId`: same bug pattern — was returning one of the many
  nested arrays (groups, roles, devices, assignedLicenses, ...) instead of the
  user object. Live-verified fix.

Also:

- `SecureScore` aliasing now covers `controlCategoryScores` and its inner
  `historicScores` array (previously only top-level, `.Scores`, and
  `.ControlProfiles` were aliased).

- `-EventType` tab completion is now dynamic — reads
  `$global:InforcerCachedEventTypes` (populated by `Get-InforcerSupportedEventType`
  at module import, refreshed on every authenticated call). New server-side event
  types show up automatically without a module release. Static fallback list
  updated to today's live 80 types (from 57).

- `ReleaseNotes` in the psd1 manifest updated for 0.6.0 (was still describing 0.5.0).

- CHANGELOG entry extended to cover all of the above.

162/162 Pester pass. Live-API verification against uk.inforcer.com: 85 assertions
pass, 5 failures are all bugs in the verification harness (hashtable-splat
mistakes), not the module.
Copilot AI review requested due to automatic review settings July 6, 2026 21:47

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 prepares the v0.6.0 release of the InforcerCommunity PowerShell module by adding a new Secure Score cmdlet, enhancing audit event querying and tab completion, and updating module documentation/testing to match the expanded public surface.

Changes:

  • Added Get-InforcerSecureScore (including formatting and property aliasing) and updated exports/version to 0.6.0.
  • Enhanced Get-InforcerAuditEvent with -User server-side filtering, an Id alias, and dynamic -EventType tab completion backed by Get-InforcerSupportedEventType.
  • Expanded inline required-scope help text across multiple cmdlets and refreshed docs/changelog accordingly.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Tests/Consistency.Tests.ps1 Updates consistency contracts and adds tests for SecureScore/AuditEvent behavior.
scripts/Test-AllCmdlets.ps1 Includes the new cmdlet in the “run all cmdlets” smoke script.
README.md Adds Get-InforcerSecureScore to the public cmdlet list.
module/Public/Invoke-InforcerAssessment.ps1 Adds required-scope line to comment-based help.
module/Public/Get-InforcerUser.ps1 Fixes -UserId detail shape by preserving API structure.
module/Public/Get-InforcerTenantPolicies.ps1 Adds required-scope line to comment-based help.
module/Public/Get-InforcerTenant.ps1 Adds required-scope line to comment-based help.
module/Public/Get-InforcerSupportedEventType.ps1 Introduces cached event-type list used for dynamic completion and adds required-scope help text.
module/Public/Get-InforcerSecureScore.ps1 New cmdlet implementation for tenant Secure Score retrieval.
module/Public/Get-InforcerRole.ps1 Adds required-scope line to comment-based help.
module/Public/Get-InforcerGroup.ps1 Adds required-scope line to comment-based help.
module/Public/Get-InforcerBaseline.ps1 Adds required-scope line to comment-based help.
module/Public/Get-InforcerAuditEvent.ps1 Adds -User filter, dynamic completer behavior, and required-scope help text.
module/Public/Get-InforcerAssessment.ps1 Adds required-scope line to comment-based help.
module/Public/Get-InforcerAlignmentDetails.ps1 Adds required-scope line to comment-based help.
module/Public/Export-InforcerTenantDocumentation.ps1 Adds required-scope line to comment-based help.
module/Public/Compare-InforcerEnvironments.ps1 Adds required-scope line to comment-based help.
module/Private/Add-InforcerPropertyAliases.ps1 Adds SecureScore aliasing and adds Id alias for AuditEvent.
module/InforcerCommunity.psd1 Bumps module version and exports Get-InforcerSecureScore + release notes update.
module/InforcerCommunity.Format.ps1xml Adds a default list view for InforcerCommunity.SecureScore.
docs/openapi-snapshot.json Updates the OpenAPI snapshot metadata.
docs/CMDLET-REFERENCE.md Adds SecureScore docs and updates AuditEvent docs (+ removes stale PolicyDiffFormatted mention).
docs/API-REFERENCE.md Adds Secure Score endpoint/schema documentation and route→scope mapping row.
CHANGELOG.md Adds the v0.6.0 release entry.

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

Comment on lines +804 to +816
controlProfiles = @(
[PSCustomObject]@{ id = 'mfa-admins'; title = 'Require MFA for admins'; controlCategory = 'Identity'; currentScore = 0; maxScore = 10; scoreDifference = 10 }
)
}
$null = Add-InforcerPropertyAliases -InputObject $secure -ObjectType SecureScore
$secure.CurrentScore | Should -Be 412.5
$secure.MaxScore | Should -Be 640
$secure.LicensedUserCount | Should -Be 275
$secure.EnabledServices | Should -Be @('AzureAD','Exchange')
$secure.Scores[0].CreatedDateTime | Should -Be '2026-07-01'
$secure.Scores[0].CurrentScore | Should -Be 410
$secure.ControlProfiles[0].Title | Should -Be 'Require MFA for admins'
$secure.ControlProfiles[0].ScoreDifference | Should -Be 10
Copilot AI review requested due to automatic review settings July 6, 2026 21:51

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

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

module/Public/Get-InforcerSupportedEventType.ps1:68

  • The help text says this cmdlet "Requires an active Inforcer session", but the implementation returns the cached/static fallback list when not connected. This makes the help misleading for users who want to see the available tab-completion values before connecting.
    .DESCRIPTION
    Retrieves the valid event type names that can be used with Get-InforcerAuditEvent.
    This function is primarily used for tab completion of the -EventType parameter in Get-InforcerAuditEvent.
    Requires an active Inforcer session (use Connect-Inforcer to establish one).

royklo added 5 commits July 7, 2026 09:35
…l one-liner

Replace 6-line BSTR marshal + ZeroFreeBSTR try/finally with
[System.Net.NetworkCredential]::new('', $s).Password.

Behavioural equivalence verified: 8 samples (empty, unicode-emoji,
newline, tab, 512-char string, standard secrets) round-trip byte-identical.
Both paths zero the unmanaged buffer — NetworkCredential uses
ZeroFreeGlobalAllocUnicode internally, same defence guarantee.
…: array

Function had zero prod callers - the argument completers in
Get-InforcerReportType.ps1 and Invoke-InforcerReport.ps1 read
$script:InforcerReportTypeStaticKeys directly at 5 sites. Only the
Pester tests called the wrapper, and they now target the array in the
module scope for the same coverage.

Not exported in .psd1 - no public API impact.
Get-InforcerPolicyDisplayInfo had exactly one caller
(ConvertTo-InforcerDocModel) and zero direct test invocations. Merged
its 114-line body into ConvertTo-InforcerDocModel.ps1 alongside its
existing siblings Get-InforcerCategoryKey and Get-InforcerPolicyName,
which follow the same 'helpers live in the caller file' pattern.

Net: -1 private file, -31 lines. Behaviour unchanged - DocModel (66/66)
and Consistency (162/162) suites pass.
…PolicyDisplayInfo

PSScriptAnalyzer PSReviewUnusedParameter flagged both params - declared
but never referenced in the function body. The single caller stopped
passing them too. Aligns with the module's no-dead-parameters rule.

DocModel 66/66 + Consistency 162/162 pass.
Pester 6 removed Assert-MockCalled (deprecated in 5). CI on Pester 6.0.0
was hitting 5 failures with 'Should operator Be is not registered' when
loading the removed cmdlet. Replaced all 6 call sites with the modern
Should -Invoke form (same semantics, added explicit -CommandName).

Suite green on Pester 5.7.1 and 6.0.0: 397 passed, 2 skipped.

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

Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.

Comment thread Tests/Consistency.Tests.ps1
Copilot AI review requested due to automatic review settings July 7, 2026 08:47
royklo added 2 commits July 7, 2026 10:49
…iasing

v0.6.0 CHANGELOG advertises PascalCase aliases for
.ControlCategoryScores and its nested .HistoricScores, but the existing
SecureScore aliasing test only covered top-level, .Scores, and
.ControlProfiles. Copilot review flagged the regression risk. Extended
the fixture with a controlCategoryScores entry and 4 assertions.

Green on Pester 5.7.1 and 6.0.0.
Fixed issues with `Get-InforcerUser` and `Get-InforcerSecureScore` cmdlets related to API response structure. Made internal improvements including code simplifications and Pester 6 compatibility.

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

Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

module/Public/Get-InforcerSupportedEventType.ps1:68

  • The help text says this cmdlet “Requires an active Inforcer session”, but the implementation intentionally returns the cached/static fallback list when not connected (to support tab completion). Updating the description avoids confusing users who run it pre-connect.
    .DESCRIPTION
    Retrieves the valid event type names that can be used with Get-InforcerAuditEvent.
    This function is primarily used for tab completion of the -EventType parameter in Get-InforcerAuditEvent.
    Requires an active Inforcer session (use Connect-Inforcer to establish one).

Comment thread docs/API-REFERENCE.md

Returns the current and historic Microsoft Secure Score for a tenant. Includes up to 90 days of daily score history, per-category scores, and actionable control profiles.

**Required scope**: `Tenants.SecureScores.Read` (or `Tenants.Read`)
Comment on lines +598 to +600
<ListItem><Label>EnabledServices</Label><ScriptBlock>if ($_.enabledServices) { ($_.enabledServices) -join ', ' } else { '' }</ScriptBlock></ListItem>
<ListItem><Label>ScoreHistoryDays</Label><ScriptBlock>if ($_.scores) { @($_.scores).Count } else { 0 }</ScriptBlock></ListItem>
<ListItem><Label>ControlProfilesCount</Label><ScriptBlock>if ($_.controlProfiles) { @($_.controlProfiles).Count } else { 0 }</ScriptBlock></ListItem>
Copilot AI review requested due to automatic review settings July 7, 2026 08:53

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

Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.

Comment on lines +17 to 18
[System.Net.NetworkCredential]::new('', $SecureString).Password
}
Comment thread docs/API-REFERENCE.md

Returns the current and historic Microsoft Secure Score for a tenant. Includes up to 90 days of daily score history, per-category scores, and actionable control profiles.

**Required scope**: `Tenants.SecureScores.Read` (or `Tenants.Read`)
Format view improvements:
- ScoreHistory now shows day count, first→last date, and latest score
  (was just a count).
- ControlCategoryScores added to the view - previously omitted, so users
  saw controlCategoryScores=System.Object[] in raw dumps and got no
  visible per-category breakdown at a glance.
- Hint field points to $obj.Scores / .ControlCategoryScores[0].HistoricScores
  / .ControlProfiles for the full nested data.

Tests:
- 3 unused $err assignments cleared (PSSA
  PSUseDeclaredVarsMoreThanAssignments). Two tests now assert on the
  error path they were meant to verify; one had no assertion at all.

Suite green on Pester 5.7.1 and 6.0.0: 397 passed, 2 skipped.
royklo added 7 commits July 7, 2026 11:51
Previous ScriptBlock used a regex '-replace' on what looked like a
string, but the API deserializer returns createdDateTime as a
[DateTime]. Regex missed, and the culture-formatted ToString() bled
through - nl-NL locale saw '04/09/2026 02:00:00' instead of '2026-04-09'.

Explicit .ToString('yyyy-MM-dd', InvariantCulture) fixes it. Live-verified
against tenant 14436.
Was rendering the raw double at full precision (79,153906866614).
Now formats with '{0:F2}' - display-only, underlying property is
untouched so consumers still see the full value.

Live-verified: 79,15 (culture-aware comma matches the sibling
CurrentScore field).
…ontrolProfile view

Default SecureScore view now includes TopRecommendations - the 3
highest-scoreDifference controls, so the answer to 'what do I fix next'
is on-screen without drilling.

Each entry in .ControlProfiles now carries the
InforcerCommunity.SecureScoreControlProfile PSTypeName. New ListControl
view shows Title, ControlCategory, Service, current/max score with
potential gain, RemediationImpact, ActionUrl, Id - dropping the giant
HTML remediation blob from the default output. The full remediation
HTML is still on the object as .remediation.

Live-verified against tenant 14436.
Two new tests protect the drill-in patterns published in the docs:

1. 'drill-in patterns from CMDLET-REFERENCE examples' - exercises
   sort by ScoreDifference, filter by Id, Group-Object + Measure-Object
   aggregation on category totals, and .ControlCategoryScores.HistoricScores
   traversal. Guards against alias-pipeline regressions that would silently
   break the documented one-liners.

2. 'nested objects carry PSTypeName for the format view' - regression
   guard for the InforcerCommunity.SecureScoreControlProfile type name that
   the new ListControl view relies on. Without this, .ControlProfiles would
   render the full HTML remediation blob on every item.

Also: fixed one more pre-existing PSSA PSUseDeclaredVarsMoreThanAssignments
warning at line 1555 - test had a parent-scope $err that never got written
to because -ErrorVariable in a child scope stays isolated. Now captures
correctly and asserts the API 'Forbidden' message is extracted.

Suite: 399 passed, 2 skipped on Pester 5.7.1 and 6.0.0.
- docs/CMDLET-REFERENCE.md: replaced 4 lightweight examples with 10
  drill-in patterns (group-by-category with TotalGain, category-history
  drill-in, CSV export, Start-Process on ActionUrl, etc.). Updated the
  Example Output block to match the new default view (ScoreHistory
  date range, ControlCategoryScores inline, TopRecommendations, Hint).

- docs/API-REFERENCE.md: added PSTypeName note for
  InforcerCommunity.SecureScoreControlProfile and explained that HTML
  remediation is excluded from the view but stays on the object.

- Get-InforcerSecureScore.ps1: added two new .EXAMPLE blocks so
  Get-Help now surfaces the group-by-category and category-history
  drill-in patterns that the docs advertise.

- .gitignore: exclude .inforcer-key.local (local live-testing key file).

Suite: 399/399 on Pester 5.7.1 and 6.0.0.
…l-in docs

- Expanded the SecureScore feature bullet: enumerates every field in the
  default ListControl view including TopRecommendations, culture-safe ISO
  date range, per-category inline breakdown, and 2-decimal-capped percentage.
- Added a new bullet for the nested PSTypeName
  'InforcerCommunity.SecureScoreControlProfile' so users know why
  $s.ControlProfiles | Format-List renders a compact view instead of
  dumping the giant HTML remediation blob.
- Added a Documentation bullet cross-referencing the 10 drill-in patterns
  now surfaced in both CMDLET-REFERENCE and Get-Help.
@royklo
royklo merged commit 8e8d863 into main Jul 7, 2026
3 checks passed
@royklo
royklo deleted the release/v0.6.0 branch July 7, 2026 14:48
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