Skip to content
Closed
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.

The format follows [Conventional Commits](https://www.conventionalcommits.org/). Versioning deviates from strict SemVer: every shipped change (feat / fix / perf / non-breaking refactor) bumps MINOR; only breaking changes bump MAJOR; docs/tests/chore-only commits don't bump. There is intentionally no `[Unreleased]` section — every entry is dated at ship time.

## [0.6.0] - 2026-07-06

### Features

- **New cmdlet: `Get-InforcerSecureScore`** — retrieves the current and historic Microsoft Secure Score for a tenant from `GET /beta/tenants/{tenantId}/secureScores`. Returns the current score, max score, licensed user count, enabled services, up to 90 days of daily score history, per-category scores, and actionable control profiles with remediation guidance. `-TenantId` accepts numeric ID, GUID, or tenant name (name/GUID resolution via `Resolve-InforcerTenantId`). Supports `-OutputType JsonObject`. Required scopes: `Tenants.SecureScores.Read` + `Tenants.Read` (only when `-TenantId` is a GUID or tenant name). PSTypeName `InforcerCommunity.SecureScore` with a ListControl default view showing current/max/percentage plus counts of nested arrays.
- **`Get-InforcerAuditEvent -User <string>`** — new parameter that server-side filters audit events by the specified user (matches the `user` field in the `POST /beta/auditEvents/search` request body). Previously users had to fetch all events and filter client-side.
- **`Id` alias on audit event output** — `AuditEvent` objects now expose the raw `id` field as PascalCase `Id`, consistent with every other object type in the module.
- **Dynamic `-EventType` tab completion** — the completer on `Get-InforcerAuditEvent -EventType` now reads `$global:InforcerCachedEventTypes`, which `Get-InforcerSupportedEventType` refreshes with the live server-side list on any authenticated call. New event types the API adds appear in tab completion automatically after the next `Get-InforcerSupportedEventType` call — no module release required. A static fallback list (currently 80 event types) covers the pre-connect case.
- **`Get-InforcerSecureScore` nested aliasing extended** — `.ControlCategoryScores` and its inner `.historicScores` now get PascalCase aliases too (previously only top-level, `.Scores`, and `.ControlProfiles` were aliased).

### Documentation

- **`Required API scope(s):` line added to `Get-Help` output for 13 cmdlets** — `Compare-InforcerEnvironments`, `Export-InforcerTenantDocumentation`, `Get-InforcerAlignmentDetails`, `Get-InforcerAssessment`, `Get-InforcerAuditEvent`, `Get-InforcerBaseline`, `Get-InforcerGroup`, `Get-InforcerRole`, `Get-InforcerSupportedEventType`, `Get-InforcerTenant`, `Get-InforcerTenantPolicies`, `Get-InforcerUser`, `Invoke-InforcerAssessment`. Users no longer need to leave PowerShell and open `docs/API-REFERENCE.md` to learn which scope a cmdlet needs. Follows the pattern already used by the Reports cmdlets.
- **Removed stale `PolicyDiffFormatted` mention** in `docs/CMDLET-REFERENCE.md` — the property was removed from the module in an earlier version (see FINDINGS #63) but one line in the docs was missed.
- Added `Get-InforcerSecureScore` section to `docs/CMDLET-REFERENCE.md`; endpoint description, schemas (`TenantSecureScoreDetails`, `TenantSecureScoreHistoryPoint`, `TenantSecureScoreControlProfile`), and scope-mapping row added to `docs/API-REFERENCE.md`; cmdlet added to the README public-surface table.

## [0.5.0] - 2026-06-30

### Features
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Disconnect-Inforcer
| **Get-InforcerUser** | Retrieves users from a tenant (list/search or detail by UserId). |
| **Get-InforcerGroup** | Retrieves Entra ID groups from a tenant (list/search or detail by GroupId). |
| **Get-InforcerRole** | Retrieves Entra ID directory role definitions from a tenant. |
| **Get-InforcerSecureScore** | Retrieves the current and 90-day historic Microsoft Secure Score for a tenant, including per-category scores and actionable control profiles. |
| **Export-InforcerTenantDocumentation** | Generates comprehensive tenant documentation in HTML, Markdown, or Excel format. |
| **Compare-InforcerEnvironments** | Compares two tenants' Intune configuration and generates an interactive HTML comparison report. Supports baseline-scoped comparison via `-SourceBaselineId` / `-DestinationBaselineId` with automatic baseline owner resolution. |
| **Get-InforcerAssessment** | Lists available assessments (CIS, Essential Eight, etc.). |
Expand Down
56 changes: 52 additions & 4 deletions Tests/Consistency.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ Describe 'Consistency contract' {
$path = Get-InforcerCommunityManifestPath
Import-Module $path -Force
$script:exported = (Get-Module -Name 'InforcerCommunity').ExportedCommands.Keys
$script:expectedCount = 20
$script:expectedCount = 21
$script:expectedNames = @(
'Connect-Inforcer', 'Disconnect-Inforcer', 'Test-InforcerConnection',
'Get-InforcerTenant', 'Get-InforcerBaseline', 'Get-InforcerTenantPolicies',
'Get-InforcerAlignmentDetails', 'Get-InforcerAuditEvent', 'Get-InforcerSupportedEventType',
'Get-InforcerUser', 'Get-InforcerGroup', 'Get-InforcerRole',
'Get-InforcerUser', 'Get-InforcerGroup', 'Get-InforcerRole', 'Get-InforcerSecureScore',
'Export-InforcerTenantDocumentation', 'Compare-InforcerEnvironments',
'Get-InforcerAssessment', 'Invoke-InforcerAssessment',
'Get-InforcerReportType', 'Invoke-InforcerReport', 'Get-InforcerReportRun', 'Save-InforcerReportOutput'
Expand All @@ -48,11 +48,12 @@ Describe 'Consistency contract' {
'Get-InforcerBaseline' = @('Format', 'TenantId', 'OutputType')
'Get-InforcerTenantPolicies' = @('Format', 'TenantId', 'OutputType')
'Get-InforcerAlignmentDetails' = @('Format', 'TenantId', 'BaselineId', 'Tag', 'OutputType')
'Get-InforcerAuditEvent' = @('EventType', 'DateFrom', 'DateTo', 'PageSize', 'MaxResults', 'Format', 'OutputType')
'Get-InforcerAuditEvent' = @('EventType', 'DateFrom', 'DateTo', 'User', 'PageSize', 'MaxResults', 'Format', 'OutputType')
'Get-InforcerSupportedEventType' = @()
'Get-InforcerUser' = @('Format', 'TenantId', 'Search', 'MaxResults', 'UserId', 'OutputType')
'Get-InforcerGroup' = @('TenantId', 'Search', 'Filter', 'MaxResults', 'Group', 'OutputType')
'Get-InforcerRole' = @('TenantId', 'OutputType')
'Get-InforcerSecureScore' = @('TenantId', 'OutputType')
'Export-InforcerTenantDocumentation' = @('Format', 'TenantId', 'OutputPath', 'SettingsCatalogPath', 'FetchGraphData', 'Baseline', 'Tag')
'Compare-InforcerEnvironments' = @('SourceTenantId', 'DestinationTenantId', 'SourceSession', 'DestinationSession', 'SourceBaselineId', 'DestinationBaselineId', 'IncludingAssignments', 'SettingsCatalogPath', 'FetchGraphData', 'ExcludeOS', 'PolicyNameFilter', 'OutputPath')
'Get-InforcerAssessment' = @('Format', 'OutputType')
Expand Down Expand Up @@ -84,7 +85,7 @@ Describe 'Consistency contract' {
}

It 'Get-* cmdlets that return API data have -OutputType' {
$getCmdlets = @('Get-InforcerTenant', 'Get-InforcerBaseline', 'Get-InforcerTenantPolicies', 'Get-InforcerAlignmentDetails', 'Get-InforcerAuditEvent', 'Get-InforcerUser', 'Get-InforcerGroup', 'Get-InforcerRole')
$getCmdlets = @('Get-InforcerTenant', 'Get-InforcerBaseline', 'Get-InforcerTenantPolicies', 'Get-InforcerAlignmentDetails', 'Get-InforcerAuditEvent', 'Get-InforcerUser', 'Get-InforcerGroup', 'Get-InforcerRole', 'Get-InforcerSecureScore')
foreach ($name in $getCmdlets) {
$cmd = Get-Command -Name $name -ErrorAction Stop
$cmd.Parameters.Keys | Should -Contain 'OutputType'
Expand Down Expand Up @@ -195,6 +196,12 @@ Describe 'No-silent-failure contract' {
$err | Should -Not -BeNullOrEmpty -Because 'should report not connected, not return silence'
}

It 'Get-InforcerSecureScore produces an error when not connected' {
$err = $null
Get-InforcerSecureScore -TenantId 1 -ErrorVariable err -ErrorAction SilentlyContinue
$err | Should -Not -BeNullOrEmpty -Because 'should report not connected, not return silence'
}

It 'Export-InforcerTenantDocumentation produces an error when not connected' {
$err = $null
Export-InforcerTenantDocumentation -TenantId 1 -ErrorVariable err -ErrorAction SilentlyContinue
Expand Down Expand Up @@ -782,6 +789,47 @@ Describe 'Private helpers (via module scope)' {
$role.IsPrivileged | Should -BeTrue
}
}

It 'SecureScore: adds top-level and nested aliases' {
& (Get-Module InforcerCommunity) {
$secure = [PSCustomObject]@{
currentScore = 412.5
currentScorePercentage = 64.45
maxScore = 640
licensedUserCount = 275
enabledServices = @('AzureAD','Exchange')
scores = @(
[PSCustomObject]@{ createdDateTime = '2026-07-01'; currentScore = 410; maxScore = 640 }
)
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
}
}

It 'AuditEvent: adds Id alias' {
& (Get-Module InforcerCommunity) {
$ev = [PSCustomObject]@{
id = 'e1a5-1234'
eventType = 'authentication'
timestamp = '2026-07-01T00:00:00Z'
user = 'admin@contoso.com'
}
$null = Add-InforcerPropertyAliases -InputObject $ev -ObjectType AuditEvent
$ev.Id | Should -Be 'e1a5-1234'
$ev.EventType | Should -Be 'authentication'
}
}
}

Context 'Filter-InforcerResponse' {
Expand Down
67 changes: 67 additions & 0 deletions docs/API-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This document describes the Inforcer REST API endpoints, schemas, and response s
- [Users](#users)
- [Groups](#groups)
- [Roles](#roles)
- [Secure Scores](#secure-scores)
- [Reports](#reports)
- [Schemas](#schemas)
- [BaselineGroup](#baselinegroup)
Expand All @@ -39,6 +40,9 @@ This document describes the Inforcer REST API endpoints, schemas, and response s
- [TenantGroupSummary](#tenantgroupsummary)
- [TenantGroup](#tenantgroup)
- [TenantRole](#tenantrole)
- [TenantSecureScoreDetails](#tenantsecurescoredetails)
- [TenantSecureScoreHistoryPoint](#tenantsecurescorehistorypoint)
- [TenantSecureScoreControlProfile](#tenantsecurescorecontrolprofile)
- [ReportType](#reporttype)
- [ReportRun](#reportrun)
- [ReportOutput](#reportoutput)
Expand Down Expand Up @@ -95,6 +99,7 @@ Built mechanically by tracing each public cmdlet through the module (including t
| `Get-InforcerUser` | `GET /beta/tenants/{tenantId}/users[/...] ` + `GET /beta/tenants` *(GUID/name lookup)* | `Tenants.Users.Read` + `Tenants.Read`† |
| `Get-InforcerGroup` | `GET /beta/tenants/{tenantId}/groups[/...] ` + `GET /beta/tenants` *(GUID/name lookup)* | `Tenants.Groups.Read` + `Tenants.Read`† |
| `Get-InforcerRole` | `GET /beta/tenants/{tenantId}/roles` + `GET /beta/tenants` *(GUID/name lookup)* | `Tenants.Roles.Read` + `Tenants.Read`† |
| `Get-InforcerSecureScore` | `GET /beta/tenants/{tenantId}/secureScores` + `GET /beta/tenants` *(GUID/name lookup)* | `Tenants.SecureScores.Read` + `Tenants.Read`† |
| `Get-InforcerAssessment` | `GET /beta/assessments` | `Assessments.Read` |
| `Invoke-InforcerAssessment` | `GET /beta/tenants`, `GET /beta/assessments`, `POST /beta/tenants/{tenantId}/assessments/{assessmentId}/runs` | `Tenants.Read` + `Assessments.Read` + `Assessments.Run` |
| `Export-InforcerTenantDocumentation` | `GET /beta/tenants`, `GET /beta/baselines`, `GET /beta/tenants/{tenantId}/policies` | `Tenants.Read` + `Baselines.Read` + `tenants.policies.Read` |
Expand Down Expand Up @@ -311,6 +316,20 @@ Returns the list of Entra ID directory role definitions for a tenant.

**Response**: Array of [TenantRole](#tenantrole) objects.

### Secure Scores

#### `GET /beta/tenants/{tenantId}/secureScores`

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`)

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `tenantId` | path | integer | Yes | Inforcer tenant ID. |

**Response**: A single [TenantSecureScoreDetails](#tenantsecurescoredetails) object.

### Reports

Beta endpoints for triggering and retrieving asynchronous report runs.
Expand Down Expand Up @@ -762,6 +781,54 @@ An Entra ID directory role definition.

**PSTypeName:** `InforcerCommunity.Role`

### TenantSecureScoreDetails

Top-level response object for `GET /beta/tenants/{tenantId}/secureScores`.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| currentScore | number | Yes | Current Microsoft Secure Score. |
| currentScorePercentage | number | No | Current score as a percentage of `maxScore`. |
| maxScore | number | Yes | Maximum possible Secure Score. |
| licensedUserCount | integer | No | Number of licensed users. |
| enabledServices | array\<string\> | No | Services enabled for this tenant (e.g. `AzureAD`, `Exchange`). |
| scores | array\<[TenantSecureScoreHistoryPoint](#tenantsecurescorehistorypoint)\> | No | Up to 90 days of daily score history. |
| controlProfiles | array\<[TenantSecureScoreControlProfile](#tenantsecurescorecontrolprofile)\> | No | Actionable control profiles with remediation guidance. |
| controlCategoryScores | array\<object\> | No | Per-category scores and history (e.g. Identity, Data, Device). |

**PSTypeName:** `InforcerCommunity.SecureScore`

### TenantSecureScoreHistoryPoint

Single daily score entry in the `scores` array of `TenantSecureScoreDetails`.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| createdDateTime | string (datetime) | Yes | Date the score was recorded (UTC). |
| currentScore | number | Yes | Score at this date. |
| currentScorePercentage | number | No | Score expressed as a percentage. |
| maxScore | number | Yes | Maximum possible score at this date. |

### TenantSecureScoreControlProfile

Actionable control profile in the `controlProfiles` array of `TenantSecureScoreDetails`.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| id | string | Yes | Control profile identifier. |
| title | string | Yes | Human-readable title. |
| controlCategory | string | No | Category (e.g. Identity, Data). |
| service | string | No | Associated service (e.g. Azure AD, Exchange). |
| currentScore | number | No | Current score for this control. |
| currentScorePercentage | number | No | Current score as a percentage. |
| maxScore | number | No | Maximum score for this control. |
| maxScorePercentage | number | No | Maximum score as a percentage. |
| scoreDifference | number | No | Gap between current and max score (higher = larger remediation opportunity). |
| scoreDifferencePercentage | number | No | Score difference as a percentage. |
| remediation | string | No | Recommended remediation steps. |
| remediationImpact | string | No | Impact description of the remediation. |
| actionUrl | string | No | URL to take the recommended action. |

### ReportType

A catalog entry from `GET /beta/reports/types`. Field names verified against `api-uk.inforcer.com`.
Expand Down
Loading