diff --git a/CHANGELOG.md b/CHANGELOG.md index e7678e2..00941cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,70 @@ All notable changes to this project will be documented in this file. The format follows [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/): a new feature or capability bumps MINOR, a bug fix or performance change that leaves the public surface alone bumps PATCH, and docs/tests/chore-only commits don't bump. While the module is pre-1.0 a breaking change also bumps MINOR and is called out under a **Breaking Changes** heading — 1.0.0 is reserved for the point the public surface is declared stable, after which breaking changes bump MAJOR. There is intentionally no `[Unreleased]` section — every entry is dated at ship time. +## [1.0.0] - 2026-11-04 + +inforcer now has a versioned API (v1) with API clients that log in with a client ID and secret. API keys keep working, but they only reach inforcer's legacy beta API, which inforcer says is due to be retired (no date announced). 1.0.0 supports both. The login decides which API is used, and every cmdlet works with either. + +### Features + +- **`Connect-Inforcer -ClientId -ClientSecret -Region uk`** logs in with an inforcer API client and uses the v1 API. The access token is requested at connect and renewed automatically before it expires, or once after a `401`. The secret may be a `SecureString` or a string. `-PassThru`, verbose output and error records never contain the secret, key or token. +- **Every cmdlet works with both logins and returns the module's usual property names.** v1 responses are mapped to the existing names, so most scripts run unchanged. v1-only fields are added, and fields v1 does not have are absent (most visibly `ClientTenantId`; use `MsTenantId`). +- **`-TenantId` takes a Microsoft tenant ID (GUID) or a tenant name with both logins.** Numeric tenant IDs only work with an API key; with client credentials they fail with an error that says what to pass instead. +- **`Invoke-InforcerAssessment` shows inforcer's own compliance score with client credentials**, the number the portal shows. With an API key it is still `Pass ÷ (Pass + Fail)`. +- **v1 rate limits are handled:** a `429` is retried up to 3 times, honouring `Retry-After` (at most 300 s per wait). Report-file downloads are not retried on `429`. API-key behaviour is unchanged (a `429` fails at once). +- **Skipped policy types are named.** With client credentials, `Get-InforcerTenantPolicies`, Export and Compare warn when inforcer could not read a policy type, naming the type and the reason (the API's `failedPolicyTypes`). +- **`Compare-InforcerEnvironments` can compare across logins:** pass one session from each login with `-SourceSession` / `-DestinationSession`. +- **Piped tenants are labelled.** Piping tenants into `Get-InforcerBaseline`, `Get-InforcerAlignmentDetails`, `Get-InforcerTenantPolicies`, `Get-InforcerUser`, `Get-InforcerGroup`, `Get-InforcerRole` or `Get-InforcerSecureScore` adds `TenantId` to each result, and `-OutputType JsonObject` returns one JSON array for all of them. Piping one tenant into `Invoke-InforcerAssessment` adds `TenantId` to its checks. Calls without pipeline input are unchanged. +- **`Compare-InforcerEnvironments` collects both tenants at the same time**, so a comparison takes about as long as its slower side. +- **Credentials never follow a redirect.** API calls, the token request, report downloads, assessment runs and the `Connect-Inforcer` check stop at an HTTP 3xx with a message naming the target. + +### Changes for API-key users + +API-key output is identical to 0.8.0 (checked live, side by side), except for these fixes and decisions: + +- `Connect-Inforcer -ApiKey` prints a warning that API keys use the legacy beta API and how to switch to an API client. +- `Connect-Inforcer -BaseUrl` must be `https://`. A host without `/api` now gets it appended. +- `Connect-Inforcer -PassThru` returns only the session hashtable. It used to return the session plus a status object, so the documented `$s = Connect-Inforcer -PassThru` failed when passed to `-SourceSession`. +- **Piping several tenants now handles every tenant** in `Get-InforcerTenantPolicies`, `Get-InforcerBaseline`, `Get-InforcerAlignmentDetails` and `Export-InforcerTenantDocumentation`; before, only the last piped tenant was processed. `Invoke-InforcerAssessment` collects piped tenants into one multi-tenant run, the same as passing them to `-TenantId`. +- `Export-InforcerTenantDocumentation -FetchGraphData` shows the Microsoft Learn names for 11 Conditional Access apps. For example, `01cb2876-…` is Device Registration Service (it said Global Secure Access) and `de8bc8b5-…` is Graph Explorer (it said Microsoft Graph Command Line Tools). +- `Get-InforcerSupportedEventType` returns the live list of event types. A parsing bug made it always fall back to its built-in list of 80, and a list of exactly one type now comes back as one type. +- `Get-InforcerUser -OutputType JsonObject` returns JSON instead of failing with "cannot call a method on a null-valued expression". +- `Get-InforcerReportRun -IncludeOutputs` / `-Wait`: each embedded output carries `runId`, so the documented `… | ForEach-Object { $_.outputs } | Save-InforcerReportOutput` bulk download works. +- Error messages: HTTP 400 errors show the API's title, detail and field errors. Network failures carry the `Inforcer API request failed:` prefix. Export and Compare report a failed policy fetch as a short message instead of a stack trace. `-ErrorVariable` holds the module's `ApiRequestFailed_` record. +- Security: `$Error` and `Get-Error` no longer contain the API key after a failed call (0.8.0 kept it in the request headers), and `Test-InforcerConnection -Verbose` no longer prints part of the key. +- Export and Compare no longer label a report with another tenant's name when a tenant record is missing. +- `Invoke-InforcerReport`: TAB completion of assessment IDs works before `-ReportType` is bound. +- **Report downloads** (`Invoke-InforcerReport`, `Save-InforcerReportOutput`) never overwrite an existing file: a taken name is saved as `name (1).ext`, `name (2).ext`, and so on. A server file name that starts with a dot gets a leading underscore (`.csv` becomes `_.csv`), and `-FileName` writes your name directly. On Windows the files are marked as downloaded from the internet, so Office opens them in Protected View. `Invoke-InforcerReport -Open` no longer launches `.xls` files; it opens the folder. +- **Redirects:** when the API answers with a redirect, the call (including `Connect-Inforcer` and assessment runs) fails with a clear message instead of following it. +- **Exported files neutralise formulas:** Excel documentation writes values that start with `=` as text (`'=…`) and no longer turns web addresses into links; `Invoke-InforcerAssessment` CSV files prefix values that start with `=`, `+`, `-`, `@`, a tab or a carriage return with `'`. Markdown documentation encodes `&`, `<` and `>` (same text once rendered), and HTML reports show names with quotes or markup as plain text. +- **The settings catalog updates again** when a newer catalog is released (it never refreshed after the first download), and a broken or oversized download no longer replaces the cached catalog. +- **Piping tenants into `Get-InforcerBaseline` or `Get-InforcerAlignmentDetails`** reads baselines and scores once per call, so results no longer repeat. +- `Get-InforcerAlignmentDetails -Tag` with a tag no tenant has returns no rows and a warning; it used to return every row. The same goes for piped tenants that do not exist. +- `Get-InforcerAuditEvent` without `-EventType` makes one search instead of one per 10 event types. Events come back in the API's order instead of grouped by type, `-MaxResults` returns the first events of that order, and the call no longer refreshes the `-EventType` completion list. Events whose type the old per-type search missed, such as `apiKeyCreate` and `apiClientCreate`, are now returned. +- `Export-InforcerTenantDocumentation`: + - The "Profile type" column is filled again (from the policy type id; the legacy API stopped sending the name). + - Entra, SharePoint and Microsoft 365 settings policies show the name inforcer gives them. "Require Multifactor Authentication to register or join devices" moves to Device settings and "Enable guest self-service sign up via user flows" to External Identities. + - Piping several tenants with `-OutputPath` set to a file stops at the second tenant with an `OutputPathIsFileForSeveralTenants` error instead of overwriting the file. `-Show` opens each HTML file once. +- **`-FetchGraphData`** (Connect, Export, Compare) asks for `Directory.Read.All`, `Policy.Read.All` and `DeviceManagementConfiguration.Read.All`, so a consent prompt can appear once. Names come from the tenant for custom directory roles and every Conditional Access application, one tenant's names never show up in another tenant's document, and a call Graph refuses warns at once (naming the missing scope) instead of retrying for about 6 seconds. +- An assessment run's HTTP errors show the response body as compact JSON. + +### Migration from API key to client credentials + +1. Create an API client in the inforcer portal (Configure > REST API) and grant the permissions for the cmdlets you use. The secret is shown once; keep it in a vault or as a `SecureString`. +2. Replace `Connect-Inforcer -ApiKey $key` with `Connect-Inforcer -ClientId $id -ClientSecret $secret`. +3. Replace numeric tenant IDs with the Microsoft tenant ID or tenant name, and read `MsTenantId` instead of `ClientTenantId`. + +The full per-object difference list is in [docs/API-REFERENCE.md](docs/API-REFERENCE.md#v1-api-client-credentials); the login table is in the README. + +### Documentation + +- README, CMDLET-REFERENCE and API-REFERENCE cover both logins, the migration steps, the v1 permissions per cmdlet and a short note on personal data in output files. +- New: the Graph scopes and endpoints `-FetchGraphData` uses, the download rules for report files, redirect handling, the alignment-details field mapping, and per-endpoint sections for assessments and alignment details. Endpoint lists, example output and help examples were checked against the code (examples use tenant names or GUIDs, so they work with both logins). + +### Tests + +- About 330 new Pester tests cover the v1 transport, token renewal, field mapping, every cmdlet on v1, error scrubbing, pipeline input, download safety, redirect refusal, settings-catalog refresh and Graph enrichment (828 pass in total). + ## [0.8.0] - 2026-10-22 ### Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e16474a..59202b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to the Inforcer PowerShell Module +# Contributing to the inforcer PowerShell Module Thank you for your interest in contributing. This project is maintained by the community. Below is how to contribute via fork and pull request, how to report bugs, and what we expect from code changes. @@ -32,7 +32,7 @@ Thank you for your interest in contributing. This project is maintained by the c - **PowerShell 7.0+** is required. - No build step: the module is a script module. After editing files under `module/`, re-import: ```powershell - Import-Module ./module -Force + Import-Module ./module/InforcerCommunity.psd1 -Force ``` - Optional: run the full cmdlet verification script from repo root: ```powershell @@ -43,9 +43,10 @@ Thank you for your interest in contributing. This project is maintained by the c The module follows a **consistency contract** so all cmdlets behave predictably: -- **Parameter order:** Format → TenantId → Tag (if any) → OutputType for Get-* cmdlets that return API data. +- **Parameter order:** Format → TenantId → BaselineId (if any) → Tag (if any) → OutputType for Get-* cmdlets that return API data. - **-Format and -OutputType:** Present where applicable; do not remove them. -- **Property names:** Use the standard PascalCase aliases (Tenant, Baseline, Policy, AlignmentScore, AuditEvent) implemented in `module/Private/Add-InforcerPropertyAliases.ps1`. +- **Property names:** keep the API casing. `module/Private/Add-InforcerPropertyAliases.ps1` adds only real renames (`BaselineId` ← `id`, `FileSize` ← `sizeBytes`, …); never add case-only aliases, which would duplicate keys in JSON and CSV output. +- **Piped tenants:** a cmdlet that takes `-TenantId` from the pipeline adds `TenantId` to each result (only when the result has none) and returns one JSON array for `-OutputType JsonObject`; calls without pipeline input stay unchanged. - **JSON depth:** Always 100 for `-OutputType JsonObject`. When you fix a bug or change behavior, describe what was done and how to test in your pull request. @@ -77,17 +78,36 @@ Use the **Feature request** template: From the repository root: ```powershell -Invoke-Pester ./Tests/Consistency.Tests.ps1 +Invoke-Pester ./Tests ``` -This checks that exported functions and key parameters match the consistency contract. Optionally run `scripts/Test-AllCmdlets.ps1` to verify all cmdlets load and respond correctly when not connected. +- `Tests/Consistency.Tests.ps1` checks that exported functions and key parameters match the consistency contract. +- `Tests/V1.*.Tests.ps1` are mocked suites for the v1 (client credentials) API: transport, response mapping, and each cmdlet group. They need no network and no credentials. +- Optionally run `scripts/Test-AllCmdlets.ps1` to verify all cmdlets load and respond correctly when not connected. + +### Testing both logins + +The module supports two logins and a change is only done when it works with both: + +- **Mocked (always):** every cmdlet change needs a beta (API key) test and a v1 (client credentials) test. Mock `Invoke-InforcerApiRequest` / the web layer; v1 fixtures use anonymised data (Contoso/Fabrikam, zero GUIDs) shaped like the v1 spec. +- **Live (maintainers):** needs **both** an API key and an API client (client ID and secret with the read permissions you test). Run the smoke script once per login (`Connect-Inforcer -ApiKey ...`, then `Connect-Inforcer -ClientId ... -ClientSecret ...`) and compare. Never commit keys, secrets, tokens, tenant names or tenant IDs, and use only production `*.inforcer.com` hosts in tracked files. The legacy API allows 400 calls per hour per key, so a full live run can exhaust it. + +### Auth and endpoint rules + +- **Auth headers are built only in `Get-InforcerAuthHeader`.** Never write `Inf-Api-Key` or `Authorization` anywhere else; it also renews v1 tokens. +- **Endpoints are version-neutral.** Callers pass `/tenants/{id}/policies`; the request helper adds `/beta` or `/v1` and applies the v1 renames (`roles` to `directoryRoles`, `auditEvents/eventTypes` to `auditEvents/types`, `customBaselineId` to `baselineId`). Do not hard-code `/beta/` or `/v1/` in cmdlets. +- **v1 responses are mapped to the module's property names** through `ConvertFrom-InforcerV1Response -ObjectType ...` (via the request helper's `-ObjectType`). Add a mapping there rather than special-casing a cmdlet. +- Paging goes through `Invoke-InforcerPagedRequest`; tenant lookups through `Get-InforcerTenantList` and baseline lookups through `Get-InforcerBaselineList` (both cached per session). +- Requests that carry credentials never follow redirects (`-MaximumRedirection 0`, a 3xx is an error). +- `-TenantId` parameters carry the aliases `ClientTenantId` and `MsTenantId`. +- Document every endpoint change in `docs/API-REFERENCE.md` with both the beta API-key scope and the v1 API client permission. ## Adding a new cmdlet 1. Implement in `module/Public/` following the consistency rules above, and add the function name to `FunctionsToExport` in `module/InforcerCommunity.psd1`. -2. Update `docs/CMDLET-REFERENCE.md` with synopsis, parameters, examples, and example output. +2. Update `docs/CMDLET-REFERENCE.md` with synopsis, parameters, examples, example output, and the required beta scopes and v1 permissions, and `docs/API-REFERENCE.md` for new endpoints. 3. Run the consistency tests; update `Tests/Consistency.Tests.ps1` if the expected cmdlet or parameter list changes. Thank you for contributing. \ No newline at end of file diff --git a/README.md b/README.md index 7472110..b7abce5 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ [CI](https://github.com/royklo/InforcerCommunity/actions) [License: MIT](LICENSE) -Community PowerShell module for the **Inforcer API**. Connect, query tenants, baselines, policies, alignment scores, and audit events with consistent parameters and output. +Community PowerShell module for the **inforcer API**. Connect, query tenants, baselines, policies, alignment scores, and audit events with consistent parameters and output. ## About -This module was **created by Roy Klooster** for the community. It is **not owned or officially maintained by Inforcer**; it is a community project built by a community member to make it easier to work with the Inforcer API from PowerShell. +This module was **created by Roy Klooster** for the community. It is **not owned or officially maintained by inforcer**; it is a community project built by a community member to make it easier to work with the inforcer API from PowerShell. - **Repository:** [https://github.com/royklo/InforcerCommunity](https://github.com/royklo/InforcerCommunity) - **PowerShell Gallery:** [https://www.powershellgallery.com/packages/InforcerCommunity](https://www.powershellgallery.com/packages/InforcerCommunity) @@ -27,7 +27,8 @@ InforcerCommunity/ │ └── workflows/ # build-and-test.yml ├── docs/ │ ├── CMDLET-REFERENCE.md # Parameters and example output -│ └── API-REFERENCE.md # API schemas and response structures +│ ├── API-REFERENCE.md # API schemas, response structures, permissions +│ └── openapi-snapshot*.json # Tracked inforcer API specs (beta, v1) ├── CHANGELOG.md # Release history ├── module/ # Script module (see module/README.md) │ ├── InforcerCommunity.psd1 @@ -38,13 +39,17 @@ InforcerCommunity/ │ ├── Public/ # Exported cmdlets (incl. Export-InforcerTenantDocumentation) │ └── Private/ # Helpers (API, session, aliases, renderers, Graph, etc.) └── Tests/ - └── Consistency.Tests.ps1 # Pester tests + ├── Consistency.Tests.ps1 # Pester tests + └── V1.*.Tests.ps1 # Mocked v1 (client credentials) suites ``` ## Prerequisites - **PowerShell 7.0+** (cross-platform: Windows, macOS, Linux) -- An **Inforcer API key** (from your Inforcer tenant) +- An **inforcer API client** (client ID and secret) - recommended. Create one in the inforcer portal under **Configure > REST API**, and grant it the permissions the cmdlets you use need (see [API Reference](docs/API-REFERENCE.md#api-client-permissions-v1)). +- or, **legacy**: an **inforcer API key** (still works, see [Legacy API key](#legacy-api-key-login)) + +The module picks the API from the way you log in: an API client uses the current **v1** API, an API key uses the legacy **beta** API. You never choose per cmdlet. ## Installation @@ -62,45 +67,115 @@ cd InforcerCommunity Import-Module ./module/InforcerCommunity.psd1 -Force ``` -Always run `Import-Module` from the **repository root** and use `./module/InforcerCommunity.psd1`. If you see an error about `Inforcer.Format.ps1xml` or a path containing `Inforcer-Powershell-Module`, you are loading the manifest from the wrong directory or an old copy of the repo; switch to the InforcerCommunity repo root and use the path above. +Always run `Import-Module` from the **repository root** and use `./module/InforcerCommunity.psd1`. If you see an error about `inforcer.Format.ps1xml` or a path containing `Inforcer-Powershell-Module`, you are loading the manifest from the wrong directory or an old copy of the repo; switch to the InforcerCommunity repo root and use the path above. ## Quick start +### Client credentials (recommended) + ```powershell -# Connect with your API key -Connect-Inforcer -ApiKey "your-api-key" -Region uk +# Connect with an API client. The access token lasts 60 minutes and is renewed automatically. +$secret = Read-Host -AsSecureString -Prompt 'Client secret' +Connect-Inforcer -ClientId '' -ClientSecret $secret -Region uk -# List tenants +# List tenants (licenses, secure score, baseline flag, tags, alignment) Get-InforcerTenant -# Get alignment score table +# Tenants are addressed by Microsoft tenant ID (GUID) or name +Get-InforcerTenantPolicies -TenantId '00000000-0000-0000-0000-000000000000' +Get-InforcerTenant | Get-InforcerSecureScore # pipeline binds MsTenantId; each result carries TenantId + +# Alignment score table Get-InforcerAlignmentDetails +``` + +`-TenantId` accepts the aliases `ClientTenantId` and `MsTenantId`, so objects from `Get-InforcerTenant` bind through the pipeline in both logins. Results from piped tenants carry a `TenantId` property, and `-OutputType JsonObject` returns one JSON array for all of them. -# Get policies for a tenant +### API key (legacy) + +```powershell +# Connect with your API key (prints a warning, see below) +Connect-Inforcer -ApiKey "your-api-key" -Region uk + +# Numeric Client Tenant IDs work only with an API key Get-InforcerTenantPolicies -TenantId 482 # Show policy changes (PolicyDiff on each tenant when available) Get-InforcerTenant | Select-Object ClientTenantId, TenantFriendlyName, PolicyDiff +``` + +### Documentation, comparison and cleanup + +```powershell # Generate tenant documentation as HTML. -OutputPath is what writes the file: omit it and the # DocModel is returned instead, so you can read the configuration without producing artefacts. # The report opens in your browser automatically, unless you are on a CI runner or pass -Show:$false. -Export-InforcerTenantDocumentation -Format Html -TenantId 482 -OutputPath ./docs +Export-InforcerTenantDocumentation -Format Html -TenantId 'Contoso' -OutputPath ./docs + +# Generate documentation for a specific baseline with Graph name resolution (see Microsoft Graph below) +Connect-Inforcer -ClientId '' -ClientSecret $secret -Region uk -FetchGraphData +Export-InforcerTenantDocumentation -Format Html -TenantId 'Contoso' -Baseline "Production" -FetchGraphData -OutputPath ./docs -# Generate documentation for a specific baseline with Graph group name resolution -Connect-Inforcer -ApiKey "your-api-key" -Region uk -FetchGraphData -Export-InforcerTenantDocumentation -Format Html -TenantId 482 -Baseline "Production" -FetchGraphData -OutputPath ./docs +# Document several tenants: pass a directory, one file per tenant +Get-InforcerTenant | Export-InforcerTenantDocumentation -Format Html -OutputPath ./docs -Show:$false # Disconnect when done Disconnect-Inforcer ``` +## Legacy API key login + +inforcer's API keys use the **legacy beta API**, which inforcer says is due to be retired. No retirement date has been announced; it only receives bug fixes. `Connect-Inforcer -ApiKey` keeps working and its output is unchanged, but it prints a warning on every login. Switch to client credentials when you can (next section). + +## Migrating from API key to client credentials + +| What | API key (beta) | Client credentials (v1) | +|------|----------------|--------------------------| +| Login | `Connect-Inforcer -ApiKey $key -Region uk` | `Connect-Inforcer -ClientId $id -ClientSecret $secret -Region uk` | +| Tenant ID | numeric Client Tenant ID, Microsoft tenant ID (GUID) or name | Microsoft tenant ID (GUID) or tenant name. A numeric ID fails with an error that says to use the GUID or name (`InvalidTenantId`; `TenantResolutionFailed` in `Invoke-InforcerReport`). | +| `ClientTenantId` property | present | **absent** (use `MsTenantId`) | +| `Get-InforcerTenant` | rich objects | the same rich objects, without `ClientTenantId` | +| Permissions | key scopes (`Tenants.Read`, ...) | API client permissions plus optional tenant filters, set in the portal | +| Rate limit | 400 calls/hour per key; a throttled (`429`) call fails at once | 400 calls/minute per partner; throttled calls are retried using `Retry-After` (up to 3 times, at most 300 s each). Report-file downloads are not retried on `429`. | +| Output | unchanged | the same property names; v1 responses are mapped to them. v1-only fields are added; fields v1 does not have are missing. | + +Steps: + +1. Create an API client in the inforcer portal (Configure > REST API) and grant the permissions for the cmdlets you use. Keep the secret in a vault or SecureString; it is shown once. +2. Replace the `Connect-Inforcer -ApiKey` line with the `-ClientId`/`-ClientSecret` form. +3. Replace numeric tenant IDs with the Microsoft tenant ID or name (`Get-InforcerTenant` lists them). Scripts that read `ClientTenantId` should read `MsTenantId`. + +The full list of differences per object is in the [API Reference](docs/API-REFERENCE.md#v1-api-client-credentials). + +## Security notes + +- Requests that carry your API key, client secret or access token never follow a redirect. When the API answers with a redirect, the call fails with a message naming the target, so credentials are never sent to another address. +- Report downloads (`Invoke-InforcerReport`, `Save-InforcerReportOutput`) never overwrite an existing file (a taken name becomes `name (1).ext`) and use a safe file name; on Windows the files are marked as downloaded from the internet. +- Exported files neutralise formulas: Excel documentation writes values that start with `=` as text, and assessment CSV files prefix values that start with `=`, `+`, `-`, `@`, a tab or a carriage return with `'`. + +## Microsoft Graph (optional) + +`-FetchGraphData` on `Connect-Inforcer`, `Export-InforcerTenantDocumentation` and `Compare-InforcerEnvironments` signs in to Microsoft Graph interactively (module `Microsoft.Graph.Authentication`, not installed automatically) to turn IDs into names. It requests three delegated read scopes, so a consent prompt can appear once: + +| Scope | Used for | +|-------|----------| +| `Directory.Read.All` | Group and user names (assignments, Conditional Access), directory roles including custom roles, application names in Conditional Access | +| `Policy.Read.All` | Named locations in Conditional Access | +| `DeviceManagementConfiguration.Read.All` | Assignment filters, scope tags, and compliance rules (Compare) | + +A call Graph refuses leaves those IDs as they are, with a warning that names the likely missing scope. The Graph endpoints are listed in the [API Reference](docs/API-REFERENCE.md#microsoft-graph). + +## Personal data + +Some output contains personal data (names, UPNs, email addresses, IP addresses, sign-in details): user details (`Get-InforcerUser -UserId`), audit events, report outputs, and the files written by `Export-InforcerTenantDocumentation`, `Compare-InforcerEnvironments`, `Invoke-InforcerAssessment`, `Invoke-InforcerReport` and `Save-InforcerReportOutput`. The module caches no user or audit data. It writes files only where you ask: to `-OutputPath`, or for `Invoke-InforcerReport` and `Save-InforcerReportOutput` to the current directory when `-OutputPath` is omitted (`-NoSave` writes nothing). Handle those files as personal data: keep them access-controlled and delete them when no longer needed. + ## Cmdlets | Cmdlet | Description | | ------------------------------ | ----------------------------------------------------------- | -| **Connect-Inforcer** | Establishes a secure connection to the Inforcer REST API. Supports `-FetchGraphData` to also connect Microsoft Graph. | +| **Connect-Inforcer** | Establishes a secure connection to the inforcer REST API with an API client (`-ClientId`/`-ClientSecret`, v1) or a legacy API key (`-ApiKey`, beta). Supports `-FetchGraphData` to also connect Microsoft Graph. | | **Disconnect-Inforcer** | Disconnects and clears the session (including Graph if connected). | | **Test-InforcerConnection** | Tests the API connection; returns `$true` or `$false`. | | **Get-InforcerTenant** | Retrieves tenant information (optional filter by TenantId). | @@ -120,16 +195,17 @@ Disconnect-Inforcer | **Get-InforcerReportType** | Lists the report catalog from the Reports API. Cached after first call; supports `-Key`, `-Tag`, and `-OutputFormat` filters. | | **Invoke-InforcerReport** | Queues a report run, polls until complete, and saves outputs to disk. Use `-NoWait` for async or `-NoSave` to download metadata only. | | **Get-InforcerReportRun** | Lists report runs (last 7 days, capped at 500). Use `-Wait` with `-RunId` to bypass the 4-minute list propagation lag. | -| **Save-InforcerReportOutput** | Downloads a report output to disk using the server's `Content-Disposition` filename. Pipeline-friendly for bulk download. | +| **Save-InforcerReportOutput** | Downloads a report output to disk using the server's `Content-Disposition` filename (or `-FileName`); never overwrites an existing file. Pipeline-friendly for bulk download. | For full parameter details and example output, see **[Cmdlet Reference](docs/CMDLET-REFERENCE.md)**. For detailed API schemas and response structures, see **[API Reference](docs/API-REFERENCE.md)**. ## Settings Catalog data -The `Export-InforcerTenantDocumentation` cmdlet resolves Intune Settings Catalog settingDefinitionIDs to friendly names using data from the [IntuneSettingsCatalogData](https://github.com/royklo/IntuneSettingsCatalogData) repository. This data (~65 MB) is **automatically downloaded and cached** on first use at `~/.inforcercommunity/data/settings.json` with a 24-hour TTL. +`Export-InforcerTenantDocumentation` and `Compare-InforcerEnvironments` resolve Intune Settings Catalog settingDefinitionIDs to friendly names using data from the [IntuneSettingsCatalogData](https://github.com/royklo/IntuneSettingsCatalogData) repository. This data (~65 MB) is **automatically downloaded and cached** on first use at `~/.inforcercommunity/data/settings.json` with a 24-hour TTL. -- **No manual setup required** — the module handles download, caching, and freshness checks automatically +- **No manual setup required** — the module handles download, caching, and freshness checks automatically. After 24 hours it checks the release and downloads only when a newer catalog exists. +- **Validated downloads** — a download replaces the cache only when it is a valid catalog of at most 512 MB; otherwise the cached copy is kept - **Offline support** — if the download fails, the module uses a stale cached copy (or proceeds without resolution, showing raw IDs) - **Override** — use `-SettingsCatalogPath` to point to a local `settings.json` file instead @@ -148,7 +224,7 @@ Found a bug or have a feature idea? Please open an issue: ## License -[MIT](LICENSE) — This module was created by Roy Klooster for the community. It is not owned or officially maintained by Inforcer; it is a community project built by a community member to make it easier to work with the Inforcer API from PowerShell. +[MIT](LICENSE) — This module was created by Roy Klooster for the community. It is not owned or officially maintained by inforcer; it is a community project built by a community member to make it easier to work with the inforcer API from PowerShell. --- diff --git a/Tests/Consistency.Tests.ps1 b/Tests/Consistency.Tests.ps1 index 683191b..4d7b0d8 100644 --- a/Tests/Consistency.Tests.ps1 +++ b/Tests/Consistency.Tests.ps1 @@ -41,7 +41,7 @@ Describe 'Consistency contract' { 'Get-InforcerReportType', 'Invoke-InforcerReport', 'Get-InforcerReportRun', 'Save-InforcerReportOutput' ) $script:expectedParameters = @{ - 'Connect-Inforcer' = @('ApiKey', 'Region', 'BaseUrl', 'FetchGraphData', 'PassThru') + 'Connect-Inforcer' = @('ApiKey', 'ClientId', 'ClientSecret', 'Region', 'BaseUrl', 'FetchGraphData', 'PassThru') 'Disconnect-Inforcer' = @() 'Test-InforcerConnection' = @() 'Get-InforcerTenant' = @('Format', 'TenantId', 'OutputType') @@ -92,6 +92,14 @@ Describe 'Consistency contract' { } } + It 'Every cmdlet that takes pipeline input has a process block' { + $missing = foreach ($cmd in (Get-Command -Module InforcerCommunity -CommandType Function)) { + $piped = $cmd.Parameters.Values | Where-Object { $_.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and ($_.ValueFromPipeline -or $_.ValueFromPipelineByPropertyName) } } + if ($piped -and -not $cmd.ScriptBlock.Ast.Body.ProcessBlock) { $cmd.Name } + } + @($missing) | Should -BeNullOrEmpty -Because 'without process {} only the last piped object is handled' + } + It 'Every exported cmdlet has comment-based help (Synopsis and at least one Example)' { foreach ($name in $script:expectedNames) { $help = Get-Help -Name $name -ErrorAction Stop @@ -629,7 +637,7 @@ Describe 'Private helpers (via module scope)' { Context 'Resolve-InforcerBaselineId' { It 'Passes through a valid GUID' { & (Get-Module InforcerCommunity) { - $guid = '91e0b0f7-69f1-453f-8d73-5a6f726b5b21' + $guid = '00000000-0000-0000-0000-000000000004' $result = Resolve-InforcerBaselineId -BaselineId $guid $result | Should -Be $guid } @@ -676,7 +684,7 @@ Describe 'Private helpers (via module scope)' { Context 'Resolve-InforcerGroupId' { It 'Passes through a valid GUID' { & (Get-Module InforcerCommunity) { - $guid = 'f44f2f5c-3160-420b-900d-5ecbede954fc' + $guid = '00000000-0000-0000-0000-000000000006' $result = Resolve-InforcerGroupId -GroupId $guid -TenantId 1 $result | Should -Be $guid } @@ -684,7 +692,7 @@ Describe 'Private helpers (via module scope)' { It 'Passes through a GUID with mixed case' { & (Get-Module InforcerCommunity) { - $guid = 'F44F2F5C-3160-420B-900D-5ECBEDE954FC' + $guid = 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' $result = Resolve-InforcerGroupId -GroupId $guid -TenantId 1 $result | Should -Be $guid } @@ -789,7 +797,7 @@ Describe 'Private helpers (via module scope)' { It 'GroupSummary: adds PascalCase aliases' { & (Get-Module InforcerCommunity) { $group = [PSCustomObject]@{ - id = 'f44f2f5c-3160-420b-900d-5ecbede954fc' + id = '00000000-0000-0000-0000-000000000006' displayName = 'All Company' description = 'Default group' mail = 'allcompany@contoso.com' @@ -797,7 +805,7 @@ Describe 'Private helpers (via module scope)' { groupTypes = @('Unified') } $null = Add-InforcerPropertyAliases -InputObject $group -ObjectType GroupSummary - $group.Id | Should -Be 'f44f2f5c-3160-420b-900d-5ecbede954fc' + $group.Id | Should -Be '00000000-0000-0000-0000-000000000006' $group.DisplayName | Should -Be 'All Company' $group.Description | Should -Be 'Default group' $group.Mail | Should -Be 'allcompany@contoso.com' @@ -809,7 +817,7 @@ Describe 'Private helpers (via module scope)' { It 'Group: adds PascalCase aliases including detail properties' { & (Get-Module InforcerCommunity) { $group = [PSCustomObject]@{ - id = 'f44f2f5c-3160-420b-900d-5ecbede954fc' + id = '00000000-0000-0000-0000-000000000006' displayName = 'All Company' description = 'Default group' mail = 'allcompany@contoso.com' @@ -823,7 +831,7 @@ Describe 'Private helpers (via module scope)' { members = @([PSCustomObject]@{ id = 'u1'; displayName = 'John'; type = '#microsoft.graph.user' }) } $null = Add-InforcerPropertyAliases -InputObject $group -ObjectType Group - $group.Id | Should -Be 'f44f2f5c-3160-420b-900d-5ecbede954fc' + $group.Id | Should -Be '00000000-0000-0000-0000-000000000006' $group.DisplayName | Should -Be 'All Company' $group.MailNickname | Should -Be 'allcompany' $group.MailEnabled | Should -BeTrue @@ -1249,7 +1257,7 @@ Describe 'Private helpers (via module scope)' { ) Mock -ModuleName InforcerCommunity Test-InforcerSession { $true } Mock -ModuleName InforcerCommunity Invoke-InforcerApiRequest { - if ($Endpoint -eq '/beta/tenants') { + if (($Endpoint -replace '^/beta', '') -eq '/tenants') { @([PSCustomObject]@{ clientTenantId = 1; tenantFriendlyName = 'Contoso' }, [PSCustomObject]@{ clientTenantId = 2; tenantFriendlyName = 'Fabrikam' }) } else { @([PSCustomObject]@{ id = 'a1'; name = 'Test Assessment' }) @@ -1272,6 +1280,40 @@ Describe 'Private helpers (via module scope)' { $json[0].ComplianceScore | Should -Be 50 } + It 'uses the API score (v1 summary.score) when the run carries one, also for the HTML report' { + Mock -ModuleName InforcerCommunity Invoke-InforcerAssessmentRun { [PSCustomObject]@{ results = $script:runResults; summary = [PSCustomObject]@{ total = 6; assessed = 4; passed = 2; failed = 2; notRun = 2; score = 49 } } } + $json = Invoke-InforcerAssessment -TenantId 1, 2 -AssessmentId 'a1' -OutputType JsonObject 6>$null | ConvertFrom-Json + $json[0].ComplianceScore | Should -Be 49 + $json[0].Passed | Should -Be 2 + $path = Join-Path $TestDrive 'api-score.html' + Invoke-InforcerAssessment -TenantId 1 -AssessmentId 'a1' -OutputPath $path 6>$null | Out-Null + Get-Content -Raw $path | Should -Match '49%' + } + + It 'ignores an API score that is not a number from 0 to 100 (