Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ee4a2cf
Dev to hotfix (#2032)
JohnDuprey Apr 28, 2026
5a13af0
Dev to hf (#2033)
KelvinTegelaar Apr 29, 2026
bd676d3
Dev to hotfix (#2036)
JohnDuprey May 2, 2026
6871d26
Dev to hotfix (#2047)
JohnDuprey May 8, 2026
7d02b0b
Major Update
DamienMatthys Jun 8, 2026
b68c93f
Update Push-CIPPDBCacheData.ps1
DamienMatthys Jun 8, 2026
4c020dd
Update Set-CIPPDBCacheDefenderCVEs.ps1
DamienMatthys Jun 8, 2026
4f8bc79
Dev to release (#2081)
KelvinTegelaar Jun 8, 2026
1434616
Dev to hotfix (#2085)
JohnDuprey Jun 9, 2026
85ef802
Delete Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneCveSyncT…
DamienMatthys Jun 10, 2026
7ae8fcd
Update Push-NinjaOneQueue.ps1
DamienMatthys Jun 10, 2026
7420db6
Update Invoke-NinjaOneTenantSync.ps1
DamienMatthys Jun 10, 2026
4998ffe
Merge branch 'dev' into dev
DamienMatthys Jun 10, 2026
5a0ddb2
Dev to hotfix (#2088)
JohnDuprey Jun 10, 2026
ed7c362
Dev to hotfix (#2099)
JohnDuprey Jun 18, 2026
f44ff88
feat: add allTenants support for shared mailbox enabled report
kris6673 Jun 19, 2026
69394ec
feat: Support room calendar processing options
kris6673 Jun 19, 2026
05096dc
feat: Add default calendar permission handling for rooms
kris6673 Jun 20, 2026
e8cd121
Dev to hotfix (#2106)
JohnDuprey Jun 22, 2026
68595cc
feat: incident severity + resolving comment
kris6673 Jun 25, 2026
42087ae
feat: Add message trace retrieval to Push-BECRun function
MWG-Logan Jun 15, 2026
03ae94e
Add typed-response + operationId enrichment for openapi.json
tim-at-rewst Jun 26, 2026
a2f29dd
feat: direction-scoped Intune group assignment
kris6673 Jun 27, 2026
dae979b
Fix Check extension alerts repeating on every run (watermark never ad…
matstocks Jun 27, 2026
9a88ec0
feat: add support for ExcludeGroupNames in assignment functions
kris6673 Jun 27, 2026
d52ec9b
Resolve tenant variables in Win32 Custom App detection script
matstocks Jun 27, 2026
6205e97
fix: modernize and improve logging
kris6673 Jun 30, 2026
eec5f69
Fix Check extension alerts repeating on every run (watermark never ad…
KelvinTegelaar Jun 30, 2026
c6c50d7
Resolve tenant variables in Win32 Custom App detection script (#2116)
KelvinTegelaar Jun 30, 2026
36fd9c0
Fix: Modernize GDAP relationship termination logging (#2117)
KelvinTegelaar Jun 30, 2026
3198c77
Enrich generated openapi.json with typed responses and operationIds (…
KelvinTegelaar Jun 30, 2026
a3c752d
feat: Add message trace retrieval to Push-BECRun function (#2098)
KelvinTegelaar Jun 30, 2026
24e7a06
Feat: Add Intune group assignment enhancements (#2115)
KelvinTegelaar Jun 30, 2026
5994a44
Feat: Add incident severity and resolving comments (#2109)
KelvinTegelaar Jun 30, 2026
0288227
Feat: Support room calendar processing options and default permission…
KelvinTegelaar Jun 30, 2026
e801464
Feat: Add allTenants support for shared mailbox enabled report (#2103)
KelvinTegelaar Jun 30, 2026
25b4110
Feat: CVE exception management page and NinjaOne CVE sync (#2080)
KelvinTegelaar Jun 30, 2026
f965afe
Implements #6214
KelvinTegelaar Jun 30, 2026
a0b263c
chore: update version to 10.5.6
JohnDuprey Jun 30, 2026
259770d
fix: revert rerun protection on scheduled tasks
JohnDuprey Jun 30, 2026
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
349 changes: 349 additions & 0 deletions .build/Add-OpenApiResponseSchemas.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
#Requires -Version 7.0
<#
.SYNOPSIS
Enriches a CIPP openapi.json with typed 200 response schemas derived by static
analysis of the API and frontend repositories.

.DESCRIPTION
The generated CIPP spec types every request body but leaves every 200 response
as the generic StandardResults envelope. This stage fills typed per-endpoint
response schemas for the read surface, using two deterministic sources that are
already checked into the repositories (no live API calls):

1. Captured response shape baselines (CIPP/Tests/Shapes/*.json) - carry real
field types and nesting. Preferred when present.
2. Frontend table column declarations (simpleColumns in CIPP/src pages) -
carry field names only. Used when no baseline exists; fields are typed as
string and marked x-cipp-field-source: frontend so consumers know the type
is a name-only inference, not a verified type.

Endpoints with neither source keep the StandardResults envelope, which is the
correct shape for write/exec operations. Output is deterministic: the same input
repositories always produce a byte-identical spec.

.PARAMETER InputSpec
Path to the source openapi.json. Defaults to the repo-root spec relative to this
script (.build/.. ).

.PARAMETER OutputSpec
Path to write the enriched spec. Defaults to InputSpec (in-place rewrite).

.PARAMETER FrontendRepoPath
Path to a checkout of the CIPP frontend repository. Provides both the shape
baselines (Tests/Shapes) and the page column declarations (src).

.PARAMETER PassThru
Return the enriched spec object instead of only writing it. Used by tests.

.EXAMPLE
./Add-OpenApiResponseSchemas.ps1 -FrontendRepoPath ../CIPP

Rewrites the repo-root openapi.json in place with typed response schemas.
#>
[CmdletBinding()]
param(
[string]$InputSpec = (Join-Path $PSScriptRoot '..' 'openapi.json'),
[string]$OutputSpec,
[string]$FrontendRepoPath,
[switch]$PassThru
)

$ErrorActionPreference = 'Stop'

$script:CippHttpMethods = @('get', 'post', 'put', 'patch', 'delete')

function ConvertFrom-ShapeNode {
<#
.SYNOPSIS
Converts one node of a captured shape tree into an OpenAPI schema fragment.
#>
param($Node)

if ($Node -is [string]) {
switch ($Node) {
'string' { return @{ type = 'string' } }
'number' { return @{ type = 'number' } }
'bool' { return @{ type = 'boolean' } }
'datetime' { return [ordered]@{ type = 'string'; format = 'date-time' } }
# 'null' (captured as null at sample time) and 'truncated' (below the
# capture depth limit) carry no reliable type, so stay permissive.
default { return @{} }
}
}

if ($Node -is [System.Collections.IDictionary]) {
if ($Node['_type'] -eq 'array') {
return [ordered]@{ type = 'array'; items = (ConvertFrom-ShapeNode -Node $Node['_element']) }
}
$properties = [ordered]@{}
foreach ($key in ($Node.Keys | Sort-Object)) {
$properties[[string]$key] = ConvertFrom-ShapeNode -Node $Node[$key]
}
return [ordered]@{ type = 'object'; properties = $properties }
}

return @{}
}

function Get-ShapeBaselineMap {
<#
.SYNOPSIS
Maps endpoint name -> per-record OpenAPI schema, from captured shape baselines.
.DESCRIPTION
Reads only files carrying both _metadata and shape; the sibling
test-results.json and any non-baseline file is skipped. The per-record schema
is the baseline shape itself (the CIPP envelope's Results[] element).
#>
param([string]$ShapesDir)

$map = @{}
if (-not (Test-Path $ShapesDir)) {
Write-Warning "Shapes directory not found: $ShapesDir"
return $map
}

foreach ($file in (Get-ChildItem -Path $ShapesDir -Filter '*.json' | Sort-Object -Property FullName)) {
$doc = Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json -AsHashtable -Depth 100
if (-not ($doc -is [System.Collections.IDictionary] -and $doc.ContainsKey('_metadata') -and $doc.ContainsKey('shape'))) {
continue
}
$endpoint = $doc['_metadata']['endpoint']
if (-not $endpoint) { continue }
$map[$endpoint] = ConvertFrom-ShapeNode -Node $doc['shape']
}
return $map
}

function Get-FrontendColumnMap {
<#
.SYNOPSIS
Maps endpoint name -> sorted unique field names, from page simpleColumns.
.DESCRIPTION
Intent: skips conditional simpleColumns arrays to avoid non-column branch strings; false negatives beat junk fields.
Scans frontend page sources for files that pair an /api/<Endpoint> reference
with a simpleColumns array, and unions the declared column names per endpoint.
Field names are deterministic; their types are not, so callers type them as
string with a provenance marker.
#>
param([string]$SrcDir)

$map = @{}
if (-not (Test-Path $SrcDir)) {
Write-Warning "Frontend src directory not found: $SrcDir"
return $map
}

$endpointPattern = [regex]'/api/([A-Za-z0-9_]+)'
$columnsPattern = [regex]'(?s)\bsimpleColumns\s*(?:=|:)\s*(?:\{\s*)?\[(?<columns>[^\]]*)\]'
$stringPattern = [regex]'"([^"]+)"|''([^'']+)'''

$files = Get-ChildItem -Path $SrcDir -Recurse -File -Include '*.js', '*.jsx'
foreach ($file in $files) {
$text = Get-Content -LiteralPath $file.FullName -Raw
if ([string]::IsNullOrEmpty($text) -or $text -notmatch 'simpleColumns') { continue }

$endpoints = $endpointPattern.Matches($text) | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique
if (-not $endpoints) { continue }

$columns = foreach ($colMatch in $columnsPattern.Matches($text)) {
foreach ($strMatch in $stringPattern.Matches($colMatch.Groups['columns'].Value)) {
$value = if ($strMatch.Groups[1].Success) { $strMatch.Groups[1].Value } else { $strMatch.Groups[2].Value }
if ($value) { $value }
}
}
if (-not $columns) { continue }

foreach ($endpoint in $endpoints) {
if (-not $map.ContainsKey($endpoint)) { $map[$endpoint] = [System.Collections.Generic.HashSet[string]]::new() }
foreach ($column in $columns) { [void]$map[$endpoint].Add($column) }
}
}
return $map
}

function ConvertTo-ColumnRecordSchema {
<#
.SYNOPSIS
Builds a per-record object schema from a set of frontend column names.
#>
param([System.Collections.Generic.HashSet[string]]$Columns)

$properties = [ordered]@{}
foreach ($column in ($Columns | Sort-Object)) {
$properties[$column] = [ordered]@{ type = 'string'; 'x-cipp-field-source' = 'frontend' }
}
return [ordered]@{ type = 'object'; properties = $properties }
}

function ConvertTo-ResponseEnvelopeSchema {
<#
.SYNOPSIS
Wraps a per-record schema in the CIPP { Results: [...], Metadata: {...} } envelope.
#>
param($RecordSchema)

return [ordered]@{
type = 'object'
properties = [ordered]@{
Results = [ordered]@{ type = 'array'; items = $RecordSchema }
Metadata = [ordered]@{ type = 'object' }
}
}
}


function Get-CippOperationId {
<#
.SYNOPSIS
Builds the deterministic operationId for one CIPP path and method.
.DESCRIPTION
Riftwing imports OpenAPI operations by operationId. CIPP upstream does not
currently emit operationIds, so this keeps importer keys stable without
depending on display labels or external data.
#>
param(
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Method,
[Parameter(Mandatory)][string[]]$PathMethods
)

$endpointName = $Path -replace '^/api/', ''
if ($PathMethods.Count -eq 1) {
return $endpointName
}

$methodName = [System.Globalization.CultureInfo]::InvariantCulture.TextInfo.ToTitleCase($Method.ToLowerInvariant())
return "$methodName$endpointName"
}

function Add-CippOperationId {
<#
.SYNOPSIS
Injects missing operationIds and fails on duplicate operationIds.
.DESCRIPTION
Existing non-empty operationIds are preserved so this pass can retire itself
when upstream starts emitting operationIds. Duplicate operationIds are fatal
because importers commonly key operations by operationId.
#>
param([Parameter(Mandatory)][System.Collections.IDictionary]$Spec)

if (-not $Spec['paths']) { throw 'Spec has no paths.' }

$operationCount = 0
$injectedCount = 0
$operationIds = @{}

foreach ($pathEntry in $Spec['paths'].GetEnumerator()) {
$pathMethods = @($pathEntry.Value.Keys | Where-Object { $_ -in $script:CippHttpMethods })
foreach ($methodEntry in $pathEntry.Value.GetEnumerator()) {
if ($methodEntry.Key -notin $script:CippHttpMethods) { continue }

$operationCount++
$operation = $methodEntry.Value
$operationId = $operation['operationId']
if ([string]::IsNullOrWhiteSpace([string]$operationId)) {
$operationId = Get-CippOperationId -Path $pathEntry.Key -Method $methodEntry.Key -PathMethods $pathMethods
$operation['operationId'] = $operationId
$injectedCount++
}

if ($operationIds.ContainsKey($operationId)) {
throw "Duplicate operationId found: $operationId"
}
$operationIds[$operationId] = $true
}
}

return [pscustomobject]@{ Operations = $operationCount; Injected = $injectedCount; Unique = $operationIds.Count }
}

function Resolve-SpecResponse {
<#
.SYNOPSIS
Adds typed 200 response schemas to a parsed spec, in place, and returns counts.
.DESCRIPTION
The pure core of this stage: operates on an already-parsed spec hashtable and
the two endpoint maps, with no file or repository access, so it is unit
testable. Only existing 200 responses on get/post/put/patch/delete operations
are touched; everything else (including operations with no matching source) is
left exactly as found.
#>
param(
[Parameter(Mandatory)][System.Collections.IDictionary]$Spec,
[Parameter(Mandatory)][hashtable]$BaselineMap,
[Parameter(Mandatory)][hashtable]$ColumnMap
)

if (-not $Spec['paths']) { throw 'Spec has no paths.' }

$operationCount = 0
$typedCount = 0

foreach ($pathEntry in $Spec['paths'].GetEnumerator()) {
$endpoint = $pathEntry.Key -replace '^/api/', ''

$recordSchema = $null
if ($BaselineMap.ContainsKey($endpoint)) {
$recordSchema = $BaselineMap[$endpoint]
} elseif ($ColumnMap.ContainsKey($endpoint)) {
$recordSchema = ConvertTo-ColumnRecordSchema -Columns $ColumnMap[$endpoint]
}

foreach ($methodEntry in $pathEntry.Value.GetEnumerator()) {
if ($methodEntry.Key -notin $script:CippHttpMethods) { continue }
$operationCount++
if ($null -eq $recordSchema) { continue }

$responses = $methodEntry.Value['responses']
if ($null -eq $responses) { continue }

$okResponse = $responses['200']
if (-not $okResponse) { continue }

$okResponse['content'] = [ordered]@{
'application/json' = [ordered]@{ schema = (ConvertTo-ResponseEnvelopeSchema -RecordSchema $recordSchema) }
}
$typedCount++
}
}

return [pscustomobject]@{
Operations = $operationCount
Typed = $typedCount
}
}

function Add-CippResponseSchema {
<#
.SYNOPSIS
File-level orchestration: read spec + repo sources, enrich, write output.
#>
param(
[Parameter(Mandatory)][string]$InputSpec,
[Parameter(Mandatory)][string]$OutputSpec,
[Parameter(Mandatory)][string]$FrontendRepoPath,
[switch]$PassThru
)

if (-not (Test-Path $InputSpec)) { throw "Input spec not found: $InputSpec" }

$spec = Get-Content -LiteralPath $InputSpec -Raw | ConvertFrom-Json -AsHashtable -Depth 100
$baselineMap = Get-ShapeBaselineMap -ShapesDir (Join-Path $FrontendRepoPath 'Tests' 'Shapes')
$columnMap = Get-FrontendColumnMap -SrcDir (Join-Path $FrontendRepoPath 'src')

$operationIdResult = Add-CippOperationId -Spec $spec
$result = Resolve-SpecResponse -Spec $spec -BaselineMap $baselineMap -ColumnMap $columnMap
Write-Information "Operations: $($result.Operations) | typed responses added: $($result.Typed) | operationIds injected: $($operationIdResult.Injected) | unique operationIds: $($operationIdResult.Unique)" -InformationAction Continue

# Serialization is deterministic for the object this stage builds, but it does not globally canonicalize pre-existing spec keys.
[System.IO.File]::WriteAllText($OutputSpec, ($spec | ConvertTo-Json -Depth 100))

if ($PassThru) { return $spec }
}

# Run orchestration only when invoked as a script, not when dot-sourced for testing.
if ($MyInvocation.InvocationName -ne '.') {
if (-not $FrontendRepoPath) { throw 'FrontendRepoPath is required when running the script.' }
if (-not $OutputSpec) { $OutputSpec = $InputSpec }
Add-CippResponseSchema -InputSpec $InputSpec -OutputSpec $OutputSpec -FrontendRepoPath $FrontendRepoPath -PassThru:$PassThru
}
38 changes: 38 additions & 0 deletions .build/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# OpenAPI enrichment

`Add-OpenApiResponseSchemas.ps1` post-processes the generated CIPP `openapi.json`. It adds deterministic operationIds and typed `200` response schemas where response shape data can be derived from the CIPP frontend repository. It does not replace the upstream OpenAPI generator.

The enriched spec is published on each GitHub Release as the `openapi.enriched.json` release asset.

The PR check and release workflow strictly lint the CI-generated `openapi.enriched.json` with Redocly. The committed `.redocly.lint-ignore.yaml` baseline pins findings that already exist in the generated enriched spec because of upstream `openapi.json` issues. Any new Redocly error or warning that is not in the baseline fails CI.

To regenerate locally, check out the CIPP frontend repository and run:

```powershell
pwsh -NoProfile -File .build/Add-OpenApiResponseSchemas.ps1 `
-FrontendRepoPath <path-to-CIPP-frontend-checkout> `
-InputSpec ./openapi.json -OutputSpec ./openapi.enriched.json
```

If upstream `openapi.json` legitimately changes and the pinned Redocly findings must be refreshed, regenerate the enriched spec first, then regenerate the ignore baseline from that enriched output:

```powershell
pwsh -NoProfile -File .build/Add-OpenApiResponseSchemas.ps1 `
-FrontendRepoPath <path-to-CIPP-frontend-checkout> `
-InputSpec ./openapi.json -OutputSpec ./openapi.enriched.json
npx --yes @redocly/cli@2.35.1 lint ./openapi.enriched.json --generate-ignore-file
```

Do not generate the baseline from the base `openapi.json`. The lint subject is always the generated `openapi.enriched.json`.

## Known limitations

- Only `get`, `post`, `put`, `patch`, and `delete` operations are processed. `head`, `options`, and `trace` are not present in the current spec.
- Paths are assumed to start with `/api/`. All 580 current paths do.
- When a typed `200` response is added, it replaces the existing `200.content`. Today that content is only the generic `StandardResults` envelope.
- Conditional/ternary `simpleColumns` expressions are intentionally not parsed.

## Release workflow notes

- `openapi-enriched-release.yml` builds and uploads from the same tag. On `workflow_dispatch`, the `tag` input is checked out and used as the upload target. On `release: published`, the release tag is checked out and used as the upload target.
- `.github/workflows/` is gitignored in this repository, so the OpenAPI workflow files require `git add -f` when they are intentionally added or updated.
Loading