test(ft546): verify read-only shadow operation
This commit is contained in:
167
tools/shadow/Export-Ft546ShadowEvidence.ps1
Normal file
167
tools/shadow/Export-Ft546ShadowEvidence.ps1
Normal file
@@ -0,0 +1,167 @@
|
||||
[CmdletBinding(DefaultParameterSetName = 'Snapshot')]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'Snapshot')]
|
||||
[string]$SnapshotDirectory,
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'Database')]
|
||||
[string]$EvidenceServer,
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'Database')]
|
||||
[string]$EvidenceDatabase,
|
||||
[Parameter(ParameterSetName = 'Database')][string]$EvidenceUsername,
|
||||
[Parameter(ParameterSetName = 'Database')][switch]$IntegratedSecurity,
|
||||
[Parameter(Mandatory = $true)][guid]$RunId,
|
||||
[Parameter(Mandatory = $true)][string]$OutputDirectory
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
. (Join-Path $PSScriptRoot 'ShadowScriptCommon.ps1')
|
||||
|
||||
$requiredFiles = @(
|
||||
'cycles.csv', 'cycles.json', 'protocol-differences.csv',
|
||||
'algorithm-differences.csv', 'insufficient-facts.csv',
|
||||
'startup-checks.json'
|
||||
)
|
||||
$output = Resolve-ShadowDirectory $OutputDirectory -Create
|
||||
if (@(Get-ChildItem -LiteralPath $output -Force).Count -gt 0) {
|
||||
throw 'EVIDENCE_EXPORT_DIRECTORY_MUST_BE_EMPTY'
|
||||
}
|
||||
|
||||
function Convert-DataTableRows {
|
||||
param([System.Data.DataTable]$Table)
|
||||
return @($Table.Rows | ForEach-Object {
|
||||
$value = [ordered]@{}
|
||||
foreach ($column in $Table.Columns) {
|
||||
$cell = $_[$column.ColumnName]
|
||||
$value[$column.ColumnName] = if ($cell -is [DBNull]) { $null } else { $cell }
|
||||
}
|
||||
[pscustomobject]$value
|
||||
})
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ParameterSetName -eq 'Snapshot') {
|
||||
$snapshot = Resolve-ShadowDirectory $SnapshotDirectory
|
||||
foreach ($name in $requiredFiles) {
|
||||
$source = Resolve-ShadowFile (Join-Path $snapshot $name)
|
||||
Copy-Item -LiteralPath $source -Destination (Join-Path $output $name)
|
||||
}
|
||||
}
|
||||
else {
|
||||
$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
|
||||
$builder.DataSource = $EvidenceServer
|
||||
$builder.InitialCatalog = $EvidenceDatabase
|
||||
$builder.Encrypt = $true
|
||||
$builder.TrustServerCertificate = $false
|
||||
$builder.ConnectTimeout = 5
|
||||
if ($IntegratedSecurity.IsPresent) {
|
||||
$builder.IntegratedSecurity = $true
|
||||
}
|
||||
else {
|
||||
if ([string]::IsNullOrWhiteSpace($EvidenceUsername)) {
|
||||
throw 'EVIDENCE_USERNAME_REQUIRED'
|
||||
}
|
||||
$password = [Environment]::GetEnvironmentVariable(
|
||||
'FT546_SHADOW_EVIDENCE_PASSWORD', 'Process')
|
||||
if ([string]::IsNullOrWhiteSpace($password)) {
|
||||
throw 'FT546_SHADOW_EVIDENCE_PASSWORD_REQUIRED'
|
||||
}
|
||||
$builder.UserID = $EvidenceUsername
|
||||
$builder.Password = $password
|
||||
}
|
||||
$connection = New-Object System.Data.SqlClient.SqlConnection $builder.ConnectionString
|
||||
function Invoke-EvidenceQuery {
|
||||
param([string]$Sql)
|
||||
$command = $connection.CreateCommand()
|
||||
$command.CommandTimeout = 10
|
||||
$command.CommandText = $Sql
|
||||
$parameter = $command.Parameters.Add('@run_id',
|
||||
[System.Data.SqlDbType]::UniqueIdentifier)
|
||||
$parameter.Value = $RunId
|
||||
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter $command
|
||||
$table = New-Object System.Data.DataTable
|
||||
[void]$adapter.Fill($table)
|
||||
return $table
|
||||
}
|
||||
try {
|
||||
$connection.Open()
|
||||
$cycles = Invoke-EvidenceQuery @'
|
||||
SELECT cycle_id,run_id,input_batch_hash,parent_task_id,phase,route_version,
|
||||
status,reason_code,database_captured_at,completed_at,evidence_hash
|
||||
FROM dbo.shadow_cycle WHERE run_id=@run_id ORDER BY completed_at,cycle_id
|
||||
'@
|
||||
Convert-DataTableRows $cycles | Export-Csv -NoTypeInformation `
|
||||
-Encoding UTF8 -LiteralPath (Join-Path $output 'cycles.csv')
|
||||
$protocol = Invoke-EvidenceQuery @'
|
||||
SELECT d.difference_id,d.cycle_id,d.category,d.reason_code,
|
||||
d.expected_json,d.actual_json
|
||||
FROM dbo.shadow_difference d JOIN dbo.shadow_cycle c ON c.cycle_id=d.cycle_id
|
||||
WHERE c.run_id=@run_id AND d.category LIKE 'PROTOCOL_%'
|
||||
ORDER BY d.difference_id
|
||||
'@
|
||||
Convert-DataTableRows $protocol | Export-Csv -NoTypeInformation `
|
||||
-Encoding UTF8 -LiteralPath (Join-Path $output 'protocol-differences.csv')
|
||||
$algorithm = Invoke-EvidenceQuery @'
|
||||
SELECT d.difference_id,d.cycle_id,d.category,d.reason_code,
|
||||
d.expected_json,d.actual_json
|
||||
FROM dbo.shadow_difference d JOIN dbo.shadow_cycle c ON c.cycle_id=d.cycle_id
|
||||
WHERE c.run_id=@run_id AND d.category LIKE 'ALGORITHM_%'
|
||||
ORDER BY d.difference_id
|
||||
'@
|
||||
Convert-DataTableRows $algorithm | Export-Csv -NoTypeInformation `
|
||||
-Encoding UTF8 -LiteralPath (Join-Path $output 'algorithm-differences.csv')
|
||||
$insufficient = Invoke-EvidenceQuery @'
|
||||
SELECT cycle_id,parent_task_id,phase,status,reason_code,database_captured_at,
|
||||
completed_at,evidence_hash
|
||||
FROM dbo.shadow_cycle
|
||||
WHERE run_id=@run_id AND status='INSUFFICIENT_FACTS'
|
||||
ORDER BY completed_at,cycle_id
|
||||
'@
|
||||
Convert-DataTableRows $insufficient | Export-Csv -NoTypeInformation `
|
||||
-Encoding UTF8 -LiteralPath (Join-Path $output 'insufficient-facts.csv')
|
||||
$startup = Invoke-EvidenceQuery @'
|
||||
SELECT check_id,check_name,status,checked_at FROM dbo.shadow_startup_check
|
||||
WHERE checked_at >= (SELECT started_at FROM dbo.shadow_run WHERE run_id=@run_id)
|
||||
ORDER BY check_id
|
||||
'@
|
||||
Convert-DataTableRows $startup | ConvertTo-Json -Depth 6 | `
|
||||
Set-Content -LiteralPath (Join-Path $output 'startup-checks.json') `
|
||||
-Encoding UTF8
|
||||
$cycleJson = [ordered]@{
|
||||
cycles = Convert-DataTableRows $cycles
|
||||
decisions = Convert-DataTableRows (Invoke-EvidenceQuery `
|
||||
'SELECT d.* FROM dbo.shadow_decision d JOIN dbo.shadow_cycle c ON c.cycle_id=d.cycle_id WHERE c.run_id=@run_id ORDER BY d.cycle_id')
|
||||
encodedWrites = Convert-DataTableRows (Invoke-EvidenceQuery `
|
||||
'SELECT w.* FROM dbo.shadow_encoded_write w JOIN dbo.shadow_cycle c ON c.cycle_id=w.cycle_id WHERE c.run_id=@run_id ORDER BY w.cycle_id,w.write_sequence')
|
||||
differences = Convert-DataTableRows (Invoke-EvidenceQuery `
|
||||
'SELECT d.* FROM dbo.shadow_difference d JOIN dbo.shadow_cycle c ON c.cycle_id=d.cycle_id WHERE c.run_id=@run_id ORDER BY d.difference_id')
|
||||
}
|
||||
$cycleJson | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath `
|
||||
(Join-Path $output 'cycles.json') -Encoding UTF8
|
||||
}
|
||||
finally {
|
||||
if ($null -ne $connection) { $connection.Dispose() }
|
||||
[Environment]::SetEnvironmentVariable(
|
||||
'FT546_SHADOW_EVIDENCE_PASSWORD', $null, 'Process')
|
||||
}
|
||||
}
|
||||
|
||||
$files = @($requiredFiles | Sort-Object | ForEach-Object {
|
||||
$path = Resolve-ShadowFile (Join-Path $output $_)
|
||||
[pscustomobject]@{
|
||||
name = $_
|
||||
length = (Get-Item -LiteralPath $path).Length
|
||||
sha256 = Get-ShadowSha256 $path
|
||||
}
|
||||
})
|
||||
$manifest = [pscustomobject]@{
|
||||
schemaVersion = 1
|
||||
runId = $RunId.ToString()
|
||||
exportedAtUtc = [DateTime]::UtcNow.ToString('o')
|
||||
sourceMode = $PSCmdlet.ParameterSetName
|
||||
files = $files
|
||||
}
|
||||
$manifestPath = Join-Path $output 'manifest.json'
|
||||
Write-ShadowJson -Value $manifest -Path $manifestPath
|
||||
$manifestHash = Get-ShadowSha256 $manifestPath
|
||||
"$manifestHash manifest.json" | Set-Content -LiteralPath `
|
||||
(Join-Path $output 'manifest.sha256') -Encoding ASCII
|
||||
return $manifest
|
||||
Reference in New Issue
Block a user