feat(reports): Reports API cmdlets + Connect-Inforcer improvements (v0.5.0)#33
Merged
Conversation
Invoke-InforcerApiRequest now interprets all three error shapes the Inforcer
API uses (app-layer {success, message, errors}, APIM gateway {statusCode,
message}, RFC 9110 ProblemDetails {type, title, status, traceId}) and pulls
a usable message out of each. The x-correlation-id response header is logged
to the verbose stream on every request and included in error messages on
failure for support tickets.
New private helpers:
Get-InforcerHeaderValue — case-insensitive header lookup across
HttpResponseHeaders / WebHeaderCollection
/ IDictionary shapes
Protect-InforcerApiKeyInText — single redaction helper used wherever an
exception or response body could leak the
live API key into a user-facing error
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Connect-Inforcer used to validate against /beta/baselines and emit an empty
error on 401 in PowerShell 7 (the catch block checked [WebException] but PS7
raises HttpResponseException, so the status code stayed at 0). It also failed
outright for keys scoped only to non-Baselines endpoints (Reports.Read,
Assessments.Read, etc.).
The validation now uses Invoke-WebRequest -SkipHttpErrorCheck (PS7+) so 4xx
responses don't throw, and interprets the *envelope shape* alongside the
status code:
* 200 → key valid + scope present
* 4xx with Inforcer-app envelope → APIM accepted the subscription, the
{success/errorCode/errors} Inforcer app rejected the scope. That
is itself proof the key is valid.
* 401 with APIM-gateway envelope → subscription key rejected — emit a
{statusCode, message} only clear error with the API's text.
No scope is privileged for validation, so a user with Reports-only,
Assessments-only, Audit-only, etc. can now connect. Verified live against
api-uk.inforcer.com with a Reports.Read + Reports.Run key (no Baselines.Read).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four new public cmdlets cover the /beta/reports/* surface end-to-end:
Get-InforcerReportType Lists the catalog from GET /reports/types and
caches it in $script:InforcerReportTypeCache.
Filters by -Key, -Tag, -OutputFormat.
Invoke-InforcerReport Queues a run (POST /reports/runs), polls
/reports/runs/{id}/outputs until terminal, and
saves each output to -OutDir. Sync + save is the
default; -NoWait queues and returns immediately,
-NoSave polls without writing. Zip / broadcast
rules on -ReportType / -OutputFormat arrays.
Client-side guards: duplicate-pair dedup, Collate
vs collatable, unknown-parameter-key rejection.
Get-InforcerReportRun Lists runs (server cap 500 items / 7 days). -Wait
with -RunId polls /outputs to bypass the ~4-min
list-propagation lag; -IncludeOutputs embeds
outputs and uses the same lag fallback.
Save-InforcerReportOutput Binary-safe download via Invoke-InforcerRawDownload.
Pipeline-friendly (-RunId + -OutputId from output
records). Uses Content-Disposition (incl. RFC 5987
filename*=UTF-8'') with cross-platform sanitization.
Dynamic -AssessmentId tab completion has three states with sub-millisecond
cache-hit latency after the first TAB:
not connected → <Run Connect-Inforcer first>
no Assessments.Read scope → <Assessments.Read API scope required>
(denial sentinel cached on 401/403)
connected with scope → friendly names with GUIDs inserted
Supporting changes:
Resolve-InforcerReportTypeSchema catalog validation + smart defaults
Test-InforcerReportRunTerminal single-poll probe (200 terminal, 404 not)
Resolve-InforcerReportOutputFileName Content-Disposition + RFC 5987 parser
Invoke-InforcerRawDownload binary-safe GET with envelope-aware errors
Add-InforcerPropertyAliases new ObjectTypes: ReportType/Run/Output
InforcerCommunity.Format.ps1xml 4 new ListControl views
InforcerCommunity.psd1 FunctionsToExport +4 → 20 total
Disconnect-Inforcer clears all $script:Inforcer*Cache* vars
(broadened wildcard catches sentinels)
Get-InforcerAssessment primes the assessment cache as a
side-effect so the completer is instant
scripts/Test-AllCmdlets.ps1 entries for the 4 new cmdlets
Required API scopes: Reports.Read + Reports.Run for trigger flows; Reports.Read
alone for listing/downloading.
All shapes verified live against api-uk.inforcer.com beta (the catalog uses
supportedOutputFormats[] and requiredParameters[]; runs use runId, plural
reportTypes[] and outputFormats[]; outputs use id/reportType/format/sizeBytes;
no server-side fileName or contentType on output records).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs/CMDLET-REFERENCE.md Per-cmdlet sections for the 4 new cmdlets with
synopsis, parameter table, examples, fake example
output, and Required API scope(s) line. The
example outputs match the real API shape
(plural reportTypes/outputFormats arrays on runs,
minimal { RunId } on -NoWait, no per-record
fileName on outputs).
docs/API-REFERENCE.md New Reports endpoint section (5 endpoints) with
per-endpoint scope lines, request/response shape
tables, and the deliberate-404-indistinguishability
caveat on the outputs endpoint. ReportType,
ReportRun, ReportOutput schemas with field names
matching live API responses. New TOC entries.
Reports.Read + Reports.Run rows in Scope→Routes.
Per-cmdlet rows in Cmdlet→Endpoints→Required Scopes.
README.md Cmdlet table extended with the 4 new cmdlets and
the existing Assessment cmdlets.
CHANGELOG.md [Unreleased] entry covering the Reports cmdlets,
the Connect/Disconnect/Invoke-InforcerApiRequest
improvements, and the dynamic completer.
REPORTS-CMDLETS-HANDOFF.md The implementation brief carried forward.
Reports-API-Feedback.md §1-10 + Q1-10 + features 1-7 from the original
two-day probe. §11-16 new findings from live
cmdlet testing:
§11 supportedOutputFormats vs outputFormats[]
§12 runId vs id naming inconsistency
§13 outputs use format/sizeBytes vs outputFormat
/fileSize on input
§14 assessment-id is opaque string, not GUID
§15 4-min list lag also blocks -IncludeOutputs
flows, not just polling
§16 collated outputs return tenantId:0 — needs
null or a scope discriminator
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ness
Tests/Consistency.Tests.ps1
- expectedCount 16 → 20; expectedParameters covers all 4 new cmdlets
- No-silent-failure assertions for the 4 new cmdlets
- Property-alias tests for ReportType/ReportRun/ReportOutput using the
real API-shape field names (key/supportedOutputFormats/requiredParameters
on types; runId/reportTypes/outputFormats on runs;
id/reportType/format/sizeBytes on outputs)
- Resolve-InforcerReportTypeSchema: 8 cases (valid pair, auto-default
report-period, unknown type, unsupported format, collate on non-collatable,
Assessment without -AssessmentId, Assessment with -AssessmentId, unknown
parameter key)
- Resolve-InforcerReportOutputFileName: 8 cases (plain filename=, quoted
with spaces, RFC 5987 filename*=UTF-8'' percent-encoding, both forms with
filename* winning, path-component stripping, reserved-char sanitization,
empty/null fallback)
- Get-InforcerHeaderValue: case-insensitive lookups across hashtable shapes
- Test-InforcerReportRunTerminal: 200/404/error branches via Mock
- Invoke-InforcerRawDownload: bytes + filename + correlation ID on 200,
fallback DefaultFileName when Content-Disposition missing, 4xx surfaces
the API-shaped message
- Disconnect-Inforcer cache clearing across multiple $script:Inforcer*Cache*
variables (including the sentinel companion)
Tests passed: 124 / 124 (full Tests/ suite: 359 / 359)
Tests/Manual/Test-AssessmentIdCompleter.ps1
Empirical-test harness per REPORTS-CMDLETS-HANDOFF.md §5. Drives the
completer scriptblock directly across the three permission states +
the disconnect-clears-cache edge case. Validated live: 825ms first TAB
(under 2s budget), 0ms cache hit; denial sentinel correctly caches on
401/403; State 3 (not connected) returns the reconnect hint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four issues that bit a real first-time user on a Reports-only key:
1. Get-InforcerReportType -Key / -Tag / -OutputFormat had no tab completion
despite the docs implying it. Added ArgumentCompleters that prefer the
live catalog (after the first call) and fall back to an inline list of
the known 25 report types / tags / formats so TAB works even before the
cache is primed.
2. Get-InforcerReportType -Key TenantAuditReport showed `Parameters :` with
a blank value for types that take no parameters. Now renders `(none)`.
3. -OutDir → -OutputPath on Invoke-InforcerReport and Save-InforcerReportOutput
to match the existing module convention (Export-InforcerTenantDocumentation,
Invoke-InforcerAssessment, Compare-InforcerEnvironments all use -OutputPath).
4. Tenant resolution on a Reports-only key produced two opaque HTTP 403
stack traces followed by a misleading "No tenant found" message. The cmdlet
now skips the /beta/tenants lookup entirely when every -TenantId is already
a numeric Client Tenant ID (fast path), and on lookup failure surfaces a
single actionable error:
"The API key was rejected when looking up tenant 'X' against
GET /beta/tenants (this endpoint requires the Tenants.Read scope, which
a Reports-only key does not include). Pass the numeric Client Tenant ID
directly — e.g. -TenantId 14436 — or re-key with Tenants.Read to enable
name / GUID resolution."
Verified live against api-uk.inforcer.com with the Reports-only key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SESSION-HANDOFF-REPORTS.md captures the state at the end of this feature
session so a fresh context can pick up cleanly. Includes:
* Six-commit branch summary
* What's verified live vs what's still untested
* Next-session focus per the user's directive: broader testing, finding
collection, iterative UX improvements
* Locked-in design decisions a new context must NOT re-derive
* Test keys + tenant IDs already exported in the user's shell history
* Live-smoke + Pester regression one-liners
This file is intended to be deleted (or rewritten) once the PR is merged —
it has no purpose post-merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Added validation for FileName parameter in Save-InforcerReportOutput.ps1 to ensure it is not null or empty. - Improved output path handling to prevent writing to system directories with Test-InforcerSafeOutputPath. - Introduced Format-InforcerErrorDetail function to render structured error messages from API responses. - Created Get-InforcerReportTypeStaticKeys to provide static fallback keys for argument completers. - Implemented comprehensive end-to-end smoke tests in Smoke-EndToEnd.ps1 to validate cmdlet functionality without live API. - Developed Test-ReportTypeCompleter.ps1 to empirically test dynamic tab completion for report types and tags. - Added Test-InforcerSafeOutputPath to ensure output paths do not point to restricted system directories. - Enhanced error handling and output file management in Save-InforcerReportOutput.ps1.
There was a problem hiding this comment.
Pull request overview
This PR expands the InforcerCommunity PowerShell module with first-class support for the Reports API (/beta/reports/*), improves connection validation to accept any valid key regardless of scope, and upgrades API error handling to surface structured field-level details and correlation IDs for support.
Changes:
- Adds four new public Reports cmdlets (catalog, run, poll, download) plus supporting private helpers (schema validation, safe downloads, filename parsing, terminal probing).
- Updates
Connect-Inforcer/Disconnect-Inforcerfor scope-agnostic validation, cache hygiene, and best-effort report catalog priming for TAB completion. - Updates docs, formatting views, CI (run all Pester tests + add a smoke job), and introduces manual test harness scripts.
Reviewed changes
Copilot reviewed 42 out of 43 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| users/roy/downloads/ActiveUsers.json | Adds a sample report output JSON (currently contains real tenant/user identifiers). |
| Tests/Manual/Test-ReportTypeCompleter.ps1 | Manual harness for report type/tag/outputformat completers and cache edge cases. |
| Tests/Manual/Test-AssessmentIdCompleter.ps1 | Manual harness for dynamic -AssessmentId completer behavior across permission states. |
| Tests/Manual/Smoke-EndToEnd.ps1 | End-to-end mocked HTTP smoke scenario for Reports cmdlets behavior. |
| SESSION-HANDOFF-REPORTS.md | Session handoff notes and testing reference material (currently includes live keys). |
| scripts/Test-AllCmdlets.ps1 | Extends smoke validation to include the new Reports cmdlets. |
| REPORTS-CMDLETS-HANDOFF.md | Implementation brief/handoff document for the Reports cmdlets design. |
| Reports-API-Feedback.md | Captures empirical API quirks/bugs and feature requests for Reports endpoints. |
| README.md | Updates exported cmdlet list to include Reports and other missing cmdlets. |
| module/Public/Save-InforcerReportOutput.ps1 | New cmdlet to download report outputs to disk (pipeline-friendly). |
| module/Public/Invoke-InforcerReport.ps1 | New cmdlet to queue, poll, download, and optionally open report outputs. |
| module/Public/Get-InforcerReportType.ps1 | New cmdlet to fetch/cache/filter the report type catalog with completers. |
| module/Public/Get-InforcerReportRun.ps1 | New cmdlet to list runs, optionally poll via outputs endpoint, and embed outputs. |
| module/Public/Get-InforcerAssessment.ps1 | Primes assessment completion cache to support report assessment-id completion. |
| module/Public/Disconnect-Inforcer.ps1 | Clears all Inforcer*Cache* variables on disconnect to prevent stale completers. |
| module/Public/Connect-Inforcer.ps1 | Scope-agnostic key validation (envelope-shape detection) + primes Reports catalog cache. |
| module/Private/Test-InforcerSafeOutputPath.ps1 | New safety helper to deny writes into OS/system directories. |
| module/Private/Test-InforcerReportRunTerminal.ps1 | New helper to probe terminal state via GET /runs/{id}/outputs (404→200). |
| module/Private/Resolve-InforcerTenantId.ps1 | Avoids leaking tenant IDs when multiple tenants match a name (privacy hardening). |
| module/Private/Resolve-InforcerReportTypeSchema.ps1 | New catalog-driven client-side validation + smart defaults for report parameters. |
| module/Private/Resolve-InforcerReportOutputFileName.ps1 | New RFC 5987-aware filename parser + filesystem-safe sanitization. |
| module/Private/Resolve-InforcerGraphEnrichment.ps1 | Increases JSON depth to 100 for Graph batch request bodies. |
| module/Private/Protect-InforcerApiKeyInText.ps1 | New helper to redact API keys from error/verbose text. |
| module/Private/Invoke-InforcerRawDownload.ps1 | New binary-safe download helper with streaming-to-disk support. |
| module/Private/Invoke-InforcerAssessmentRun.ps1 | Fixes disposal/stop behavior to avoid runspace/socket leaks on interruption. |
| module/Private/Invoke-InforcerApiRequest.ps1 | Adds 3-envelope error parsing, structured errors[] rendering, and correlation-id capture. |
| module/Private/Get-InforcerSettingsCatalogPath.ps1 | Ensures JSON writes use -Depth 100 (avoids truncation). |
| module/Private/Get-InforcerReportTypeStaticKeys.ps1 | New central static fallback list for report type key completion. |
| module/Private/Get-InforcerHeaderValue.ps1 | New helper for consistent header retrieval across PS host header shapes. |
| module/Private/Get-InforcerComparisonData.ps1 | Ensures JSON serialization uses depth 100 (avoids truncation). |
| module/Private/Format-InforcerErrorDetail.ps1 | New helper to render structured API validation errors into readable text. |
| module/Private/Compare-InforcerDocModels.ps1 | Ensures JSON serialization uses depth 100 (avoids truncation). |
| module/Private/Add-InforcerPropertyAliases.ps1 | Extends aliasing to Reports object types (ReportType/ReportRun/ReportOutput). |
| module/InforcerCommunity.psm1 | Initializes progress ID seed and warns on module unload clearing session/caches. |
| module/InforcerCommunity.psd1 | Bumps module to 0.5.0 and exports the new Reports cmdlets. |
| module/InforcerCommunity.Format.ps1xml | Adds default views for ReportType/ReportRun/ReportOutput/ReportRunResult. |
| docs/CMDLET-REFERENCE.md | Adds reference sections and examples for the new Reports cmdlets. |
| docs/api-schema-snapshot.json | Updates schema snapshot metadata and includes Reports endpoints. |
| docs/API-REFERENCE.md | Adds Reports section, scope mappings, and schemas for report types/runs/outputs. |
| CHANGELOG.md | Adds 0.5.0 entry describing Reports feature set and related improvements. |
| .gitignore | Updates ignored local scratch files and artifacts; reshapes ignore patterns. |
| .github/workflows/build-and-test.yml | Runs all Pester tests, adds cmdlet smoke job, and improves analyzer messaging. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+362
to
+367
| # Convert tags array to comma-separated string for display | ||
| $tagsProp = $obj.PSObject.Properties['tags'] | ||
| if ($tagsProp -and $tagsProp.Value -is [array]) { | ||
| $tagsProp.Value = ($tagsProp.Value | Where-Object { $_ }) -join ', ' | ||
| } | ||
| AddAliasIfExists $obj 'Tags' 'tags' |
Comment on lines
+40
to
+42
| .PARAMETER AssessmentId | ||
| Required when ReportType is 'Assessment'. The assessment GUID. Get-InforcerAssessment | ||
| lists available IDs. |
| - **Required scopes** — `Reports.Read` + `Reports.Run` (+ `Tenants.Read` only when `-TenantId` is a GUID or tenant name). | ||
| - **New cmdlet: `Get-InforcerReportRun`** — lists report runs from `GET /beta/reports/runs` (server caps the result at 500 items / last 7 days). `-Wait` with `-RunId` polls until the run is complete and bypasses a ~4-minute lag between completion and list visibility. `-IncludeOutputs` embeds each run's outputs. | ||
| - **New cmdlet: `Save-InforcerReportOutput`** — downloads a specific report output to disk via `GET /beta/reports/runs/{runId}/outputs/{outputId}`. Pipeline-friendly with `Invoke-InforcerReport -NoSave` and `Get-InforcerReportRun -IncludeOutputs`. Uses the server's `Content-Disposition` filename (with full Unicode support); `-FileName` overrides. Sanitizes Windows reserved names (`CON`, `PRN`, `AUX`, etc.) and caps length at 200 characters while preserving the extension. | ||
| - **Dynamic tab completion for `-AssessmentId` on `Invoke-InforcerAssessment`** — three permission-aware states: not connected (hint to run `Connect-Inforcer`), connected without scope (hint about `Assessments.Read`, denial cached so subsequent TABs are instant), connected with scope (friendly names in the dropdown, GUID inserted on selection, `name — GUID` shown as the tooltip). |
Comment on lines
+87
to
+91
| | Key | Scopes | Use case | | ||
| |---|---|---| | ||
| | `4a832801726549079d90a8ca293a28e7` | `Reports.Read` + `Reports.Run` | Default test key — exercises scope-agnostic Connect + completer State 2 (denied scope) | | ||
| | `cd4f9bca64c74f7a98e8f20f13b604e1` | Above + `Assessments.Read` | Exercises completer State 1 (real assessments) + Assessment-type reports | | ||
| | `f0c47ea78c114cc081a355e6bb8053d2` | (rejected by APIM) | Tests the genuine "invalid subscription" path | |
| **Live smoke (one-liner):** | ||
|
|
||
| ```powershell | ||
| INFORCER_API_KEY='cd4f9bca64c74f7a98e8f20f13b604e1' pwsh -NoProfile -Command ' |
| @@ -0,0 +1 @@ | |||
| {"title":"Active Users","rows":[{"UserPrincipalName":"roy@inforcedbyroy.onmicrosoft.com","TenantDnsName":"inforcedbyroy.onmicrosoft.com","Id":"80808797-4441-4fb3-9c06-7974ad25c3dc"},{"UserPrincipalName":"breakglass@inforcedbyroy.onmicrosoft.com","TenantDnsName":"inforcedbyroy.onmicrosoft.com","Id":"d2c64e5d-0538-463c-a6ef-e9c88a28de4f"},{"UserPrincipalName":"jon@inforcedbyroy.onmicrosoft.com","TenantDnsName":"inforcedbyroy.onmicrosoft.com","Id":"a9d8c055-7c37-468c-944d-991e469461b8"},{"UserPrincipalName":"Tim@inforcedbyroy.onmicrosoft.com","TenantDnsName":"inforcedbyroy.onmicrosoft.com","Id":"8bdcf4c7-3b4a-4cb8-996d-c5af3c7c3a26"},{"UserPrincipalName":"Lewis@inforcedbyroy.onmicrosoft.com","TenantDnsName":"inforcedbyroy.onmicrosoft.com","Id":"4e87b958-384b-4fb3-9103-4b040ca6e192"},{"UserPrincipalName":"JonTest@inforcedbyroy.onmicrosoft.com","TenantDnsName":"inforcedbyroy.onmicrosoft.com","Id":"8b085767-0af3-4267-8891-3217dd8e5c36"},{"UserPrincipalName":"demo@inforcedbyroy.onmicrosoft.com","TenantDnsName":"inforcedbyroy.onmicrosoft.com","Id":"1ae1317b-4136-45fe-93dc-92c5e2394c1a"},{"UserPrincipalName":"mac@inforcedbyroy.onmicrosoft.com","TenantDnsName":"inforcedbyroy.onmicrosoft.com","Id":"07c145c4-1235-4295-8c48-f04ac9e4a26c"}]} No newline at end of file | |||
Comment on lines
+157
to
+159
| foreach ($e in $script:InforcerReportTypeCache) { | ||
| if (($e.PSObject.Properties['key'].Value -as [string]) -ieq $rtStr) { $entry = $e; break } | ||
| } |
Comment on lines
+26
to
+31
| .PARAMETER ReportPeriod | ||
| Optional integer days. When set, merged into parameters as 'report-period'=<value>. | ||
| .PARAMETER AssessmentId | ||
| Optional GUID. When set, merged into parameters as 'assessment-id'=<value>. Required | ||
| for ReportType='Assessment'. | ||
| .PARAMETER Collate |
Comment on lines
+48
to
+50
| # Normalise the ApiKey to SecureString. Priority: explicit -ApiKey → $env:INFORCER_API_KEY | ||
| # → interactive prompt. Plain strings are zeroed after conversion. | ||
| function ConvertTo-SafeApiKey { |
Comment on lines
+214
to
+216
| if ($PSBoundParameters.ContainsKey('AssessmentId')) { | ||
| $finalParams['assessment-id'] = $AssessmentId.ToString() | ||
| } |
Comment on lines
+159
to
+163
| ContentType = $download.ContentType | ||
| CorrelationId = $download.CorrelationId | ||
| } | ||
| $result.PSObject.TypeNames.Insert(0, 'InforcerCommunity.ReportRunResult') | ||
|
|
Comment on lines
+27
to
+28
| $pattern = [regex]::new([regex]::Escape($ApiKey), 'Compiled') | ||
| $pattern.Replace($Text, '[REDACTED]') |
…ters and improve error handling for AssessmentId
…nsitive data is excluded
Comment on lines
+392
to
+393
| | 200 | `{ data: [ReportOutput, ...] }` | Run is in a terminal state (`completed` or `completedWithErrors`) and outputs are available. | | ||
| | 200 | `{ data: [] }` | Run is visible to the API key, but **no outputs are within the key's tenant scope.** Collated outputs (covering multiple tenants) are only returned when the key covers every tenant the run targeted. | |
| @@ -0,0 +1,175 @@ | |||
| # Reports API — Feedback & Questions | |||
|
|
|||
| From two empirical-probing sessions against `api-uk.inforcer.com` (UK region, beta endpoints) on 2026-06-25 and 2026-06-26. All findings reproduced at least twice unless noted. Test scope: API key with `Reports.Read` + `Reports.Trigger` scopes, targeting tenants `14436` and `18159`. | |||
Comment on lines
+345
to
+347
| # Keep raw 'tags' shape intact (array OR comma-separated string from the API); | ||
| # downstream filters (e.g. Get-InforcerReportType -Tag) rely on the raw shape, | ||
| # and the Format.ps1xml view joins for display. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Get-InforcerReportType,Invoke-InforcerReport,Get-InforcerReportRun,Save-InforcerReportOutput— wrapping/beta/reports/*withsync+save (default),
-NoWait,-NoSave, and an-Openswitch (allowlisted extensionsonly) that launches saved files via the OS default handler.
Connect-Inforcernow accepts any valid API key regardless of scope (envelope-shapedetection); HTTP 401 errors on PowerShell 7 now surface a meaningful message; the Reports
Summary
Get-InforcerReportType,Invoke-InforcerReport,Get-InforcerReportRun,Save-InforcerReportOutput— wrapping/beta/reports/*with sync+save (default),-NoWait,-NoSave, and an-Openswitch (allowlisted extensions only) that launches saved files via the OS default handler.
Connect-Inforcernow accepts any valid API key regardless of scope (envelope-shape detection); HTTP 401 errors onPowerShell 7 now surface a meaningful message; the Reports catalog cache is best-effort primed on connect so TAB completion
works on first attempt.
errors[]array (e.g. "Validation failed —tenants.includeTenants contains tenant X that is not in the API key's scope") instead of the generic placeholder;
x-correlation-idis captured for support tickets.Test plan
Invoke-Pester ./Tests/ -Output Detailed→ 394 pass / 0 fail / 2 legit skipsImport-Module ./module/InforcerCommunity.psd1 -Force→ 20 cmdlets exported./Tests/Manual/Live-ApiSmoke.ps1 -BaseUrl '<region>' -TenantId <owned>→24/24 PASS
Invoke-InforcerReport -Key ActiveUsers -TenantId <X> -OutputFormat csv -OutputPath ./tmp -Open→ file lands on diskand opens in the OS handler
-Openallowlist denies extensions outside.csv .json .html .htm .pdf .xlsx .xls .xml .txt .zip .md .tsvConnect-Inforcersucceeds with a Reports-scoped-only key (previously failed because/beta/baselinesvalidationrequired
Baselines.Read)PSReviewUnusedParameterfalse-positivesexcluded — those params are framework contracts)
Release notes
See
CHANGELOG.md§[0.5.0] - 2026-06-30. Module version bumped from 0.4.0 to 0.5.0 inmodule/InforcerCommunity.psd1.