Modules/Core/10-Manifest.ps1
|
function Get-RangerToolVersion { # Issue #161: toolVersion was a hardcoded parameter default ('1.1.0') that was never updated # when the module version changed. Derive it dynamically from the loaded module so the manifest # always reflects the version that actually ran. $mod = Get-Module -Name 'AzureLocalRanger' -ErrorAction SilentlyContinue if ($mod -and $mod.Version) { return $mod.Version.ToString() } # Fallback: read from the manifest file next to the psm1 $psdPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\AzureLocalRanger.psd1' $resolvedPsd = [System.IO.Path]::GetFullPath($psdPath) if (Test-Path -Path $resolvedPsd) { $psd = Import-PowerShellDataFile -Path $resolvedPsd -ErrorAction SilentlyContinue if ($psd -and $psd.ModuleVersion) { return [string]$psd.ModuleVersion } } return 'unknown' } function New-RangerManifest { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Config, [Parameter(Mandatory = $true)] [object[]]$SelectedCollectors ) $targetNodes = @($Config.targets.cluster.nodes) [ordered]@{ run = [ordered]@{ toolVersion = Get-RangerToolVersion schemaVersion = Get-RangerManifestSchemaVersion startTimeUtc = (Get-Date).ToUniversalTime().ToString('o') endTimeUtc = $null mode = $Config.output.mode unattended = $false runner = $env:COMPUTERNAME includeDomains = @($Config.domains.include) excludeDomains = @($Config.domains.exclude) selectedCollectors = @($SelectedCollectors | ForEach-Object { $_.Id }) baselineManifestPath = $null drift = [ordered]@{ status = 'not-requested' summary = [ordered]@{} skippedReason = $null } schemaValidation = [ordered]@{ isValid = $null; errors = @(); warnings = @() } } target = [ordered]@{ environmentLabel = $Config.environment.name clusterName = $Config.environment.clusterName clusterFqdn = $Config.targets.cluster.fqdn resourceGroup = $Config.targets.azure.resourceGroup subscriptionId = $Config.targets.azure.subscriptionId tenantId = $Config.targets.azure.tenantId nodeList = $targetNodes } topology = [ordered]@{} collectors = [ordered]@{} domains = Get-RangerReservedDomainPayloads relationships = @() findings = @() artifacts = @() evidence = @() } } function Get-RangerManifestSchemaContract { # Issue #160: schema was previously loaded from repo-management/contracts/manifest-schema.json, # a path that only exists in the source repo and never in a PSGallery-installed module. # Embedding inline removes the file-path dependency entirely and works from any install location. return [ordered]@{ schemaVersion = '1.1.0-draft' requiredTopLevelKeys = @('run', 'target', 'topology', 'collectors', 'domains', 'relationships', 'findings', 'artifacts', 'evidence') requiredRunKeys = @('toolVersion', 'schemaVersion', 'startTimeUtc', 'mode', 'selectedCollectors') collectorStatuses = @('success', 'partial', 'failed', 'skipped', 'not-applicable') reservedDomains = @('clusterNode', 'hardware', 'storage', 'networking', 'virtualMachines', 'identitySecurity', 'azureIntegration', 'monitoring', 'managementTools', 'performance', 'oemIntegration') } } function Test-RangerManifestSchema { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [object[]]$SelectedCollectors ) $errors = New-Object System.Collections.Generic.List[string] $warnings = New-Object System.Collections.Generic.List[string] $schemaContract = Get-RangerManifestSchemaContract foreach ($requiredKey in @($schemaContract.requiredTopLevelKeys)) { if (-not $Manifest.Contains($requiredKey)) { $errors.Add("Manifest is missing required top-level key '$requiredKey'.") } } if ($Manifest.run.schemaVersion -ne $schemaContract.schemaVersion) { $warnings.Add("Manifest schemaVersion '$($Manifest.run.schemaVersion)' does not match schema contract version '$($schemaContract.schemaVersion)'.") } foreach ($runKey in @($schemaContract.requiredRunKeys)) { if (-not $Manifest.run.Contains($runKey) -or $null -eq $Manifest.run[$runKey] -or [string]::IsNullOrWhiteSpace([string]$Manifest.run[$runKey])) { $errors.Add("Manifest.run is missing required value '$runKey'.") } } foreach ($reservedDomain in @($schemaContract.reservedDomains)) { if (-not $Manifest.domains.Contains($reservedDomain)) { $errors.Add("Manifest.domains is missing reserved payload '$reservedDomain'.") } } foreach ($collectorId in @($Manifest.run.selectedCollectors)) { if (-not $Manifest.collectors.Contains($collectorId)) { $errors.Add("Manifest.collectors is missing selected collector '$collectorId'.") } } foreach ($collectorEntry in @($Manifest.collectors.GetEnumerator())) { if ($collectorEntry.Value.status -notin @($schemaContract.collectorStatuses)) { $errors.Add("Collector '$($collectorEntry.Key)' has unsupported status '$($collectorEntry.Value.status)'.") } } $artifactPaths = @($Manifest.artifacts | Where-Object { -not [string]::IsNullOrWhiteSpace($_.relativePath) } | ForEach-Object { $_.relativePath }) $duplicateArtifacts = @($artifactPaths | Group-Object | Where-Object { $_.Count -gt 1 }) foreach ($duplicateArtifact in $duplicateArtifacts) { $warnings.Add("Manifest.artifacts contains duplicate relativePath '$($duplicateArtifact.Name)'.") } if ($SelectedCollectors) { foreach ($collector in $SelectedCollectors) { if ($collector.Id -notin @($Manifest.run.selectedCollectors)) { $warnings.Add("Selected collector '$($collector.Id)' was not recorded in manifest.run.selectedCollectors.") } } } return [ordered]@{ IsValid = $errors.Count -eq 0 Errors = @($errors) Warnings = @($warnings) } } function Save-RangerCollectorEvidence { param( [Parameter(Mandatory = $true)] [hashtable]$CollectorResult, [Parameter(Mandatory = $true)] [string]$EvidenceRoot, [Parameter(Mandatory = $true)] [ref]$Manifest ) if (-not $CollectorResult.ContainsKey('RawEvidence') -or $null -eq $CollectorResult.RawEvidence) { return } New-Item -ItemType Directory -Path $EvidenceRoot -Force | Out-Null $fileName = "{0}.json" -f (Get-RangerSafeName -Value $CollectorResult.CollectorId) $filePath = Join-Path -Path $EvidenceRoot -ChildPath $fileName $CollectorResult.RawEvidence | ConvertTo-Json -Depth 100 | Set-Content -Path $filePath -Encoding UTF8 $relativePath = [System.IO.Path]::GetRelativePath((Split-Path -Parent $EvidenceRoot), $filePath) $Manifest.Value.evidence += @( [ordered]@{ collector = $CollectorResult.CollectorId kind = 'raw-evidence' path = $relativePath } ) } function Add-RangerCollectorToManifest { param( [Parameter(Mandatory = $true)] [ref]$Manifest, [Parameter(Mandatory = $true)] [hashtable]$CollectorResult, [string]$EvidenceRoot, [bool]$KeepRawEvidence = $false ) $Manifest.Value.collectors[$CollectorResult.CollectorId] = [ordered]@{ status = $CollectorResult.Status startTimeUtc = $CollectorResult.StartTimeUtc endTimeUtc = $CollectorResult.EndTimeUtc targetScope = @($CollectorResult.TargetScope) credentialScope = $CollectorResult.CredentialScope messages = @($CollectorResult.Messages) } if ($CollectorResult.ContainsKey('Topology') -and $CollectorResult.Topology) { $Manifest.Value.topology = ConvertTo-RangerHashtable -InputObject $CollectorResult.Topology } if ($CollectorResult.ContainsKey('Domains')) { foreach ($domainKey in $CollectorResult.Domains.Keys) { $Manifest.Value.domains[$domainKey] = ConvertTo-RangerHashtable -InputObject $CollectorResult.Domains[$domainKey] } } if ($CollectorResult.ContainsKey('Relationships')) { $Manifest.Value.relationships += @(ConvertTo-RangerHashtable -InputObject $CollectorResult.Relationships) } if ($CollectorResult.ContainsKey('Findings')) { $Manifest.Value.findings += @(ConvertTo-RangerHashtable -InputObject $CollectorResult.Findings) } if ($CollectorResult.ContainsKey('Evidence')) { $Manifest.Value.evidence += @(ConvertTo-RangerHashtable -InputObject $CollectorResult.Evidence) } if ($KeepRawEvidence -and $EvidenceRoot) { Save-RangerCollectorEvidence -CollectorResult $CollectorResult -EvidenceRoot $EvidenceRoot -Manifest $Manifest } } function Save-RangerManifest { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [string]$Path ) $Manifest.run.endTimeUtc = (Get-Date).ToUniversalTime().ToString('o') New-Item -ItemType Directory -Path (Split-Path -Parent $Path) -Force | Out-Null $Manifest | ConvertTo-Json -Depth 100 | Set-Content -Path $Path -Encoding UTF8 } function New-RangerPackageIndex { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [string]$ManifestPath, [Parameter(Mandatory = $true)] [string]$PackageRoot ) $index = [ordered]@{ environment = $Manifest.target.environmentLabel clusterName = $Manifest.target.clusterName mode = $Manifest.run.mode generatedAt = (Get-Date).ToUniversalTime().ToString('o') manifest = [System.IO.Path]::GetRelativePath($PackageRoot, $ManifestPath) artifacts = @($Manifest.artifacts) } $path = Join-Path -Path $PackageRoot -ChildPath 'package-index.json' $index | ConvertTo-Json -Depth 50 | Set-Content -Path $path -Encoding UTF8 return $path } |