Private/New-SqlCertInventoryRow.ps1
|
# ============================================================================= # Script : Private/New-SqlCertInventoryRow.ps1 # Author : Keith Ramsey # Created : 2026-09-07 # ============================================================================= # Change Log # ----------------------------------------------------------------------------- # 2026-09-07 Keith Ramsey Initial: SqlCert.Inventory row factory for the D1 # estate-wide inventory (GR-001, DR-026/027/028). # One row per certificate OR per unreadable surface; # expiry reported not judged (Unknown-not-false). # ============================================================================= # Decision Contract (Docs/DECISIONS_PHASE7.md) # ----------------------------------------------------------------------------- # Must : SqlCert.Inventory shape with Status Found|Skipped|Failed (DR-026); # DaysToExpiry/Expiring computed from NotAfter, and BOTH $null (Unknown) # when NotAfter is unreadable -- never 0/false (DR-028); pure factory, # no state change, never throws; provenance-stamped like SqlCert.Result. # ============================================================================= function New-SqlCertInventoryRow { <# .SYNOPSIS Builds one SqlCert.Inventory row for a certificate found on a surface, or for a surface that could not be read. .DESCRIPTION Pure factory for the estate-wide inventory (D1). Normalises a certificate found on any surface -- engine, RS/PBIRS, TDE, backup, endpoint, cell -- to a single shape, and computes expiry per DR-028: DaysToExpiry is whole days from now (UTC) and Expiring is true at or below -ThresholdDays. When -NotAfter is not supplied (an unreadable date, or a Skipped/Failed surface row) DaysToExpiry and Expiring are both $null (Unknown) -- never 0/false, which would read as "fine" and hide the gap. Never throws. .PARAMETER Surface The cert surface this row describes: Engine, ReportingServices, Tde, Backup, Endpoint, or Cell. .PARAMETER SqlInstance The instance the row pertains to (may be empty for a host-level read). .PARAMETER Node The computer the row pertains to. Defaults to the local machine. .PARAMETER Thumbprint The certificate thumbprint, when a certificate was found. .PARAMETER Subject The certificate subject, when a certificate was found. .PARAMETER NotAfter The certificate's validity end. Omit for an unreadable date or a Skipped/Failed surface row -> expiry becomes Unknown ($null). .PARAMETER ThresholdDays Days-to-expiry at or below which Expiring is set. Default 30 (DR-023/028). .PARAMETER Status Found (a certificate), Skipped (surface absent), or Failed (surface present but unreadable). Default Found. .PARAMETER Detail Human-readable context; the reason on a Skipped/Failed row. .OUTPUTS SqlCert.Inventory (one object). .EXAMPLE New-SqlCertInventoryRow -Surface Tde -SqlInstance MSSQLSERVER ` -Thumbprint $tp -Subject 'CN=tde' -NotAfter (Get-Date).AddDays(20) .EXAMPLE New-SqlCertInventoryRow -Surface Backup -Status Skipped -Detail 'No encrypted backups on this instance.' .NOTES Steps: 1. Compute DaysToExpiry/Expiring from NotAfter when present; both $null (Unknown) when absent (DR-028). 2. Read the provenance stamp from the module's single source (fallbacks keep a standalone unit test honest). 3. Emit the SqlCert.Inventory pscustomobject. Pure; never throws. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure in-memory inventory-row factory; performs no state change.')] [CmdletBinding()] [OutputType('SqlCert.Inventory')] param( [Parameter(Mandatory)] [ValidateSet('Engine', 'ReportingServices', 'Tde', 'Backup', 'Endpoint', 'Cell')] [string] $Surface, [string] $SqlInstance = '', [string] $Node = $env:COMPUTERNAME, [string] $Thumbprint = '', [string] $Subject = '', [Nullable[datetime]] $NotAfter, [int] $ThresholdDays = 30, [ValidateSet('Found', 'Skipped', 'Failed')] [string] $Status = 'Found', [string] $Detail = '' ) # 1. Expiry per DR-028: reported, not judged; Unknown ($null) when no date. $daysToExpiry = $null $expiring = $null if ($null -ne $NotAfter) { $daysToExpiry = [int][math]::Floor(([datetime]$NotAfter).ToUniversalTime().Subtract([datetime]::UtcNow).TotalDays) $expiring = ($daysToExpiry -le $ThresholdDays) } # 2. Provenance: single source is SqlCertForge.psm1; fallbacks for standalone tests. $mv = if (Test-Path variable:script:moduleVersion) { $script:moduleVersion } else { 'unknown' } $sv = if (Test-Path variable:script:schemaVersion) { $script:schemaVersion } else { 'unknown' } $sha = if (Test-Path variable:script:commitSha) { $script:commitSha } else { $null } $rid = if (Test-Path variable:script:runId) { $script:runId } else { $null } # 3. Emit the row. [pscustomobject]@{ PSTypeName = 'SqlCert.Inventory' Surface = $Surface SqlInstance = $SqlInstance Node = $Node Thumbprint = $Thumbprint Subject = $Subject NotAfter = $NotAfter DaysToExpiry = $daysToExpiry Expiring = $expiring Status = $Status Detail = $Detail ModuleVersion = $mv SchemaVersion = $sv CommitSha = $sha RunId = $rid Timestamp = [datetime]::UtcNow.ToString('o') } } |