168 lines
7.1 KiB
PowerShell
168 lines
7.1 KiB
PowerShell
[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
|