Private/Definition.ps1
|
# Check Definitions and where a Run's Definitions came from. # # A Check Definition is data: it names a Kind the module implements and supplies that # Kind's parameters. It can never introduce behaviour (ADR-0001). # # There are exactly two sources and no cache. A Run either fetched from the Checks Repo or # it used the Local Definitions shipped inside the module, and the Report says which. A Run # that used one while claiming the other is the silent failure this design exists to # prevent, so resolution keeps the Checks and the Provenance in one object and never # assembles them separately. function Get-LocalDefinitionPath { [CmdletBinding()] param() Join-Path $PSScriptRoot '..\Definitions\checks.json' | Resolve-Path | Select-Object -ExpandProperty Path } function Read-CheckDefinitionDocument { <# .SYNOPSIS Reads a Check Definition document from a file into the shape a Run uses. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)][string]$Path) ConvertFrom-CheckDefinitionJson -Document (Get-Content -Path $Path -Raw -Encoding UTF8) } function ConvertFrom-CheckDefinitionJson { <# .SYNOPSIS Turns a Check Definition document's JSON into the shape a Run uses. .DESCRIPTION Separate from reading a file so that a document fetched from the Checks Repo and one shipped inside the module go through exactly the same parse. Two parsers would eventually disagree, and a Definition that behaves differently depending on where it came from is the failure Provenance exists to make visible. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)][AllowEmptyString()][string]$Document) # Not $document: PowerShell variable names are case-insensitive, so assigning the # parsed object back over a [string] parameter coerces it straight back into a string # and every property below reads as empty - silently, and only at runtime. $content = $Document | ConvertFrom-Json $generated = $null if ($content.Generated) { $parsedDate = [datetime]::MinValue if ([datetime]::TryParse([string]$content.Generated, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref]$parsedDate)) { $generated = $parsedDate } } $checks = @($content.Checks | Where-Object { $_ } | ForEach-Object { [pscustomobject]@{ PSTypeName = 'Gutcheck.CheckDefinition' Name = $_.Name Kind = $_.Kind Parameters = ConvertTo-ParameterHashtable $_.Parameters MinimumModuleVersion = $_.MinimumModuleVersion } }) [pscustomobject]@{ PSTypeName = 'Gutcheck.CheckDefinitionDocument' Version = [string]$content.Version Generated = $generated Check = $checks } } function Resolve-CheckDefinition { <# .SYNOPSIS Decides which Check Definitions a Run uses, and states where they came from. .PARAMETER Fetched The document fetched from the Checks Repo, or $null when no fetch happened. There is no third state: an absent fetch means Local Definitions, not a stale cache. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)][string]$LocalPath, [AllowNull()]$Fetched, [Parameter(Mandatory)][version]$ModuleVersion ) if ($Fetched) { $document = $Fetched $source = Get-Text 'Provenance.Source.Published' } else { $document = Read-CheckDefinitionDocument -Path $LocalPath $source = Get-Text 'Provenance.Source.Local' } [pscustomobject]@{ PSTypeName = 'Gutcheck.ResolvedDefinitions' Check = $document.Check Provenance = [pscustomobject]@{ PSTypeName = 'Gutcheck.Provenance' Source = $source Version = $document.Version Generated = $document.Generated ModuleVersion = $ModuleVersion } } } function Get-ProvenanceStatement { <# .SYNOPSIS The sentence every Report carries saying which Definitions produced it. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)]$Provenance, [datetime]$AsOf = (Get-Date) ) if ($null -eq $Provenance.Generated) { $age = Get-Text 'Provenance.Age.Unknown' } else { $days = [int][math]::Floor(($AsOf.Date - ([datetime]$Provenance.Generated).Date).TotalDays) if ($days -le 0) { $age = Get-Text 'Provenance.Age.Today' } elseif ($days -eq 1) { $age = Get-Text 'Provenance.Age.OneDay' } else { $age = Get-Text 'Provenance.Age.Days' $days } } (Get-Text 'Provenance.Statement') -f ` $Provenance.Source, $Provenance.Version, $age, $Provenance.ModuleVersion } function New-ProvenanceFinding { <# .SYNOPSIS Provenance as a Finding, so it reaches a Technician who reads only the Findings. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)]$Provenance, [datetime]$AsOf = (Get-Date) ) New-Finding -Category Gutcheck -Check (Get-Text 'Check.Definition.CheckDefinitions') -Severity INFO ` -Value (Get-ProvenanceStatement -Provenance $Provenance -AsOf $AsOf) } |