527 lines
20 KiB
PowerShell
527 lines
20 KiB
PowerShell
[CmdletBinding(DefaultParameterSetName = 'SqlCredential')]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateLength(1, 260)]
|
|
[string]$Server,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$')]
|
|
[string]$Database,
|
|
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'SqlCredential')]
|
|
[Management.Automation.PSCredential]$Credential,
|
|
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'WindowsCredential')]
|
|
[switch]$UseWindowsAuthentication,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ProfilePath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
|
|
[string]$ExpectedProfileSha256,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$CliPath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
|
|
[string]$ExpectedCliSha256,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^[A-Fa-f0-9]{40}$')]
|
|
[string]$ExpectedSignerThumbprint,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
|
|
[string]$ExpectedCollectorSha256,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SnapshotOutputPath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ReportOutputPath,
|
|
|
|
[ValidateRange(5, 60)]
|
|
[int]$ConnectionTimeoutSeconds = 15,
|
|
|
|
[ValidateRange(5, 120)]
|
|
[int]$CommandTimeoutSeconds = 60
|
|
)
|
|
|
|
Set-StrictMode -Version 2.0
|
|
$ErrorActionPreference = 'Stop'
|
|
if ($PSVersionTable.PSVersion -lt [Version]'5.1' -or
|
|
[string]$PSVersionTable.PSEdition -ne 'Desktop' -or
|
|
[string]::IsNullOrWhiteSpace($env:SystemRoot)) {
|
|
throw 'lserp_select_only_profile_preflight_failed:windows_powershell_51_required'
|
|
}
|
|
|
|
$utf8 = [Text.UTF8Encoding]::new($false, $true)
|
|
$maximumProfileBytes = 1024 * 1024
|
|
$maximumCollectorBytes = 2 * 1024 * 1024
|
|
$maximumCliBytes = 64 * 1024 * 1024
|
|
$maximumResponseCharacters = 4 * 1024 * 1024
|
|
$maximumReportBytes = 1024 * 1024
|
|
$safeSha256 = '^[a-f0-9]{64}$'
|
|
$safeCorrelationId = '^[A-Za-z0-9_.:-]{8,128}$'
|
|
$profileLock = $null
|
|
$collectorLock = $null
|
|
$cliLock = $null
|
|
$snapshotFull = $null
|
|
$reportFull = $null
|
|
$snapshotCreated = $false
|
|
$reportCreated = $false
|
|
$reportPublished = $false
|
|
$cliText = $null
|
|
|
|
function Throw-ProfilePreflightError([string]$Code) {
|
|
throw ('lserp_select_only_profile_preflight_failed:' + $Code)
|
|
}
|
|
|
|
function Assert-NoReparseDirectoryChain([string]$Directory, [string]$Code) {
|
|
try {
|
|
$current = [IO.DirectoryInfo]::new([IO.Path]::GetFullPath($Directory))
|
|
while ($null -ne $current) {
|
|
if (-not $current.Exists -or
|
|
(($current.Attributes -band
|
|
[IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
Throw-ProfilePreflightError $Code
|
|
}
|
|
$current = $current.Parent
|
|
}
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_profile_preflight_failed:')) { throw }
|
|
Throw-ProfilePreflightError $Code
|
|
}
|
|
}
|
|
|
|
function Resolve-RegularFile(
|
|
[string]$Path,
|
|
[long]$MaximumBytes,
|
|
[string]$Code
|
|
) {
|
|
try {
|
|
$full = [IO.Path]::GetFullPath($Path)
|
|
if (-not [IO.File]::Exists($full)) {
|
|
Throw-ProfilePreflightError $Code
|
|
}
|
|
$item = Get-Item -LiteralPath $full -Force
|
|
if ($item.PSIsContainer -or
|
|
$item.Length -le 0 -or
|
|
$item.Length -gt $MaximumBytes -or
|
|
(($item.Attributes -band
|
|
[IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
Throw-ProfilePreflightError $Code
|
|
}
|
|
Assert-NoReparseDirectoryChain $item.DirectoryName $Code
|
|
return $full
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_profile_preflight_failed:')) { throw }
|
|
Throw-ProfilePreflightError $Code
|
|
}
|
|
}
|
|
|
|
function Resolve-NewJsonPath([string]$Path, [string]$Code) {
|
|
try {
|
|
$full = [IO.Path]::GetFullPath($Path)
|
|
if ([IO.Path]::GetExtension($full) -ine '.json' -or
|
|
[IO.File]::Exists($full) -or
|
|
[IO.Directory]::Exists($full)) {
|
|
Throw-ProfilePreflightError $Code
|
|
}
|
|
$directory = [IO.Path]::GetDirectoryName($full)
|
|
if ([string]::IsNullOrWhiteSpace($directory)) {
|
|
Throw-ProfilePreflightError $Code
|
|
}
|
|
Assert-NoReparseDirectoryChain $directory $Code
|
|
return $full
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_profile_preflight_failed:')) { throw }
|
|
Throw-ProfilePreflightError $Code
|
|
}
|
|
}
|
|
|
|
function Get-Sha256FromOpenStream([IO.FileStream]$Stream) {
|
|
$algorithm = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$Stream.Position = 0
|
|
$hash = $algorithm.ComputeHash($Stream)
|
|
$Stream.Position = 0
|
|
return ([BitConverter]::ToString($hash)).Replace(
|
|
'-', '').ToLowerInvariant()
|
|
}
|
|
finally { $algorithm.Dispose() }
|
|
}
|
|
|
|
function Get-Sha256Bytes([byte[]]$Bytes) {
|
|
$algorithm = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
return ([BitConverter]::ToString(
|
|
$algorithm.ComputeHash($Bytes))).Replace(
|
|
'-', '').ToLowerInvariant()
|
|
}
|
|
finally { $algorithm.Dispose() }
|
|
}
|
|
|
|
function Test-ExactProperties([object]$Value, [string[]]$Expected) {
|
|
if ($null -eq $Value) { return $false }
|
|
$names = @($Value.PSObject.Properties | ForEach-Object { $_.Name })
|
|
if ($names.Count -ne $Expected.Count) { return $false }
|
|
foreach ($name in $Expected) {
|
|
if ($names -cnotcontains $name) { return $false }
|
|
}
|
|
return $true
|
|
}
|
|
|
|
function Assert-Boolean([object]$Value, [string]$Code) {
|
|
if ($Value -isnot [bool]) { Throw-ProfilePreflightError $Code }
|
|
}
|
|
|
|
function Assert-Sha256([object]$Value, [string]$Code) {
|
|
if ($Value -isnot [string] -or
|
|
([string]$Value) -cnotmatch $safeSha256) {
|
|
Throw-ProfilePreflightError $Code
|
|
}
|
|
}
|
|
|
|
function Read-VerifiedCliEnvelope([string]$Text, [int]$ExitCode) {
|
|
try {
|
|
if ([string]::IsNullOrWhiteSpace($Text) -or
|
|
$Text.Length -gt $maximumResponseCharacters) {
|
|
Throw-ProfilePreflightError 'cli_response_invalid'
|
|
}
|
|
$document = $Text | ConvertFrom-Json
|
|
if (-not (Test-ExactProperties $document `
|
|
@('ok', 'correlationId', 'data')) -or
|
|
$document.ok -isnot [bool] -or
|
|
-not $document.ok -or
|
|
([string]$document.correlationId) -cnotmatch $safeCorrelationId -or
|
|
$null -eq $document.data -or
|
|
$ExitCode -notin @(0, 6)) {
|
|
Throw-ProfilePreflightError 'cli_response_invalid'
|
|
}
|
|
$expectedDataProperties = @(
|
|
'schemaVersion', 'verificationType', 'snapshotSha256',
|
|
'profileSha256', 'catalogDatabaseScopeFingerprint',
|
|
'toolSha256Verified', 'freshnessVerified',
|
|
'permissionGateVerified', 'permissionRecheckVerified',
|
|
'safetyVerified', 'toolSourceBytesStableVerified',
|
|
'databaseIdentityMatches', 'databaseMetadataMatches',
|
|
'criticalCatalogContractMatches',
|
|
'missingCriticalCatalogEntryCount',
|
|
'missingCriticalCatalogEntrySha256', 'catalogSetSha256',
|
|
'onlineMetadataMatches', 'registrationReady')
|
|
if (-not (Test-ExactProperties $document.data `
|
|
$expectedDataProperties) -or
|
|
[string]$document.data.schemaVersion -cne '1.1' -or
|
|
[string]$document.data.verificationType -cne
|
|
'select_only_catalog_snapshot') {
|
|
Throw-ProfilePreflightError 'cli_response_invalid'
|
|
}
|
|
foreach ($name in @(
|
|
'toolSha256Verified', 'freshnessVerified',
|
|
'permissionGateVerified', 'permissionRecheckVerified',
|
|
'safetyVerified', 'toolSourceBytesStableVerified',
|
|
'databaseIdentityMatches', 'databaseMetadataMatches',
|
|
'criticalCatalogContractMatches', 'onlineMetadataMatches',
|
|
'registrationReady')) {
|
|
Assert-Boolean $document.data.$name 'cli_response_invalid'
|
|
}
|
|
foreach ($name in @(
|
|
'snapshotSha256', 'profileSha256',
|
|
'catalogDatabaseScopeFingerprint', 'catalogSetSha256')) {
|
|
Assert-Sha256 $document.data.$name 'cli_response_invalid'
|
|
}
|
|
$missing = @($document.data.missingCriticalCatalogEntrySha256)
|
|
$missingCount = 0
|
|
if (-not [int]::TryParse(
|
|
[string]$document.data.missingCriticalCatalogEntryCount,
|
|
[ref]$missingCount) -or
|
|
$missingCount -lt 0 -or $missingCount -gt 4096 -or
|
|
$missing.Count -ne $missingCount) {
|
|
Throw-ProfilePreflightError 'cli_response_invalid'
|
|
}
|
|
$prior = $null
|
|
foreach ($hash in $missing) {
|
|
Assert-Sha256 $hash 'cli_response_invalid'
|
|
if ($null -ne $prior -and
|
|
[string]::CompareOrdinal($prior, [string]$hash) -ge 0) {
|
|
Throw-ProfilePreflightError 'cli_response_invalid'
|
|
}
|
|
$prior = [string]$hash
|
|
}
|
|
$expectedOnline = [bool]$document.data.databaseIdentityMatches -and
|
|
[bool]$document.data.databaseMetadataMatches -and
|
|
[bool]$document.data.criticalCatalogContractMatches
|
|
if (-not $document.data.toolSha256Verified -or
|
|
-not $document.data.freshnessVerified -or
|
|
-not $document.data.permissionGateVerified -or
|
|
-not $document.data.permissionRecheckVerified -or
|
|
-not $document.data.safetyVerified -or
|
|
-not $document.data.toolSourceBytesStableVerified -or
|
|
$document.data.registrationReady -or
|
|
[bool]$document.data.onlineMetadataMatches -ne $expectedOnline -or
|
|
(($ExitCode -eq 0) -ne $expectedOnline) -or
|
|
(($ExitCode -eq 6) -ne (-not $expectedOnline))) {
|
|
Throw-ProfilePreflightError 'cli_response_invalid'
|
|
}
|
|
return $document
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_profile_preflight_failed:')) { throw }
|
|
Throw-ProfilePreflightError 'cli_response_invalid'
|
|
}
|
|
}
|
|
|
|
function New-RestrictedFileSecurity {
|
|
try {
|
|
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
$localSystem = [Security.Principal.SecurityIdentifier]::new(
|
|
[Security.Principal.WellKnownSidType]::LocalSystemSid,
|
|
$null)
|
|
$security = New-Object Security.AccessControl.FileSecurity
|
|
$security.SetOwner($currentUser)
|
|
$security.SetAccessRuleProtection($true, $false)
|
|
$allow = [Security.AccessControl.AccessControlType]::Allow
|
|
foreach ($identity in @($currentUser, $localSystem)) {
|
|
$rule = [Security.AccessControl.FileSystemAccessRule]::new(
|
|
$identity,
|
|
[Security.AccessControl.FileSystemRights]::FullControl,
|
|
$allow)
|
|
[void]$security.AddAccessRule($rule)
|
|
}
|
|
return $security
|
|
}
|
|
catch {
|
|
Throw-ProfilePreflightError 'report_acl_invalid'
|
|
}
|
|
}
|
|
|
|
function Publish-RestrictedReport([string]$Path, [object]$Report) {
|
|
$stream = $null
|
|
$writer = $null
|
|
try {
|
|
$json = ConvertTo-Json -InputObject $Report -Depth 8 -Compress
|
|
$byteCount = $utf8.GetByteCount($json)
|
|
if ($byteCount -le 0 -or $byteCount -gt $maximumReportBytes) {
|
|
Throw-ProfilePreflightError 'report_size_invalid'
|
|
}
|
|
$security = New-RestrictedFileSecurity
|
|
$stream = [IO.FileStream]::new(
|
|
$Path,
|
|
[IO.FileMode]::CreateNew,
|
|
[Security.AccessControl.FileSystemRights]::Write,
|
|
[IO.FileShare]::None,
|
|
4096,
|
|
[IO.FileOptions]::WriteThrough,
|
|
$security)
|
|
$script:reportCreated = $true
|
|
$writer = [IO.StreamWriter]::new($stream, $utf8, 4096, $false)
|
|
$stream = $null
|
|
$writer.Write($json)
|
|
$writer.Flush()
|
|
$writer.Dispose()
|
|
$writer = $null
|
|
|
|
$item = Get-Item -LiteralPath $Path -Force
|
|
$acl = [IO.File]::GetAccessControl($Path)
|
|
if ($item.Length -le 0 -or
|
|
$item.Length -gt $maximumReportBytes -or
|
|
(($item.Attributes -band
|
|
[IO.FileAttributes]::ReparsePoint) -ne 0) -or
|
|
-not $acl.AreAccessRulesProtected) {
|
|
Throw-ProfilePreflightError 'report_publish_invalid'
|
|
}
|
|
}
|
|
finally {
|
|
if ($null -ne $writer) { $writer.Dispose() }
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
}
|
|
|
|
try {
|
|
$collectorFull = Resolve-RegularFile (Join-Path $PSScriptRoot `
|
|
'Invoke-LserpSelectOnlyCatalogSnapshot.ps1') `
|
|
$maximumCollectorBytes 'collector_file_invalid'
|
|
$profileFull = Resolve-RegularFile $ProfilePath `
|
|
$maximumProfileBytes 'profile_file_invalid'
|
|
$cliFull = Resolve-RegularFile $CliPath `
|
|
$maximumCliBytes 'cli_file_invalid'
|
|
if ([IO.Path]::GetFileName($cliFull) -cne 'lserp-cli.exe') {
|
|
Throw-ProfilePreflightError 'cli_file_invalid'
|
|
}
|
|
$snapshotFull = Resolve-NewJsonPath $SnapshotOutputPath `
|
|
'snapshot_output_invalid'
|
|
$reportFull = Resolve-NewJsonPath $ReportOutputPath `
|
|
'report_output_invalid'
|
|
if ($snapshotFull -ceq $reportFull) {
|
|
Throw-ProfilePreflightError 'output_paths_conflict'
|
|
}
|
|
|
|
$profileLock = [IO.File]::Open(
|
|
$profileFull, [IO.FileMode]::Open, [IO.FileAccess]::Read,
|
|
[IO.FileShare]::Read)
|
|
$collectorLock = [IO.File]::Open(
|
|
$collectorFull, [IO.FileMode]::Open, [IO.FileAccess]::Read,
|
|
[IO.FileShare]::Read)
|
|
$cliLock = [IO.File]::Open(
|
|
$cliFull, [IO.FileMode]::Open, [IO.FileAccess]::Read,
|
|
[IO.FileShare]::Read)
|
|
|
|
$profileSha256 = Get-Sha256FromOpenStream $profileLock
|
|
$collectorSha256 = Get-Sha256FromOpenStream $collectorLock
|
|
$cliSha256 = Get-Sha256FromOpenStream $cliLock
|
|
if ($profileSha256 -cne $ExpectedProfileSha256.ToLowerInvariant()) {
|
|
Throw-ProfilePreflightError 'profile_hash_mismatch'
|
|
}
|
|
if ($collectorSha256 -cne
|
|
$ExpectedCollectorSha256.ToLowerInvariant()) {
|
|
Throw-ProfilePreflightError 'collector_hash_mismatch'
|
|
}
|
|
if ($cliSha256 -cne $ExpectedCliSha256.ToLowerInvariant()) {
|
|
Throw-ProfilePreflightError 'cli_hash_mismatch'
|
|
}
|
|
|
|
$signature = Get-AuthenticodeSignature -LiteralPath $cliFull
|
|
$actualSignerThumbprint = if ($null -eq $signature.SignerCertificate) {
|
|
''
|
|
} else {
|
|
([string]$signature.SignerCertificate.Thumbprint).Replace(
|
|
' ', '').ToUpperInvariant()
|
|
}
|
|
if ($signature.Status -ne
|
|
[Management.Automation.SignatureStatus]::Valid -or
|
|
$actualSignerThumbprint -cne
|
|
$ExpectedSignerThumbprint.ToUpperInvariant()) {
|
|
Throw-ProfilePreflightError 'cli_signature_invalid'
|
|
}
|
|
|
|
$collectorArguments = @{
|
|
Server = $Server
|
|
Database = $Database
|
|
OutputPath = $snapshotFull
|
|
ConnectionTimeoutSeconds = $ConnectionTimeoutSeconds
|
|
CommandTimeoutSeconds = $CommandTimeoutSeconds
|
|
}
|
|
if ($PSCmdlet.ParameterSetName -ceq 'SqlCredential') {
|
|
$collectorArguments.Credential = $Credential
|
|
}
|
|
else {
|
|
$collectorArguments.UseWindowsAuthentication = $true
|
|
}
|
|
& $collectorFull @collectorArguments | Out-Null
|
|
if (-not [IO.File]::Exists($snapshotFull)) {
|
|
Throw-ProfilePreflightError 'snapshot_not_created'
|
|
}
|
|
$snapshotCreated = $true
|
|
$snapshotSha256 = (Get-FileHash -LiteralPath $snapshotFull `
|
|
-Algorithm SHA256).Hash.ToLowerInvariant()
|
|
|
|
$correlationId = 'catalog-preflight-' + [Guid]::NewGuid().ToString('N')
|
|
$cliOutput = @(& $cliFull `
|
|
'adapters' 'verify-catalog-snapshot' `
|
|
'--input' $snapshotFull `
|
|
'--profile' $profileFull `
|
|
'--tool-sha256' $collectorSha256 `
|
|
'--correlation-id' $correlationId 2>&1)
|
|
$cliExitCode = $LASTEXITCODE
|
|
$cliText = (($cliOutput | ForEach-Object { [string]$_ }) -join `
|
|
[Environment]::NewLine)
|
|
$envelope = Read-VerifiedCliEnvelope $cliText $cliExitCode
|
|
$data = $envelope.data
|
|
if ([string]$data.snapshotSha256 -cne $snapshotSha256 -or
|
|
[string]$data.profileSha256 -cne $profileSha256) {
|
|
Throw-ProfilePreflightError 'cli_artifact_binding_invalid'
|
|
}
|
|
if ((Get-Sha256FromOpenStream $profileLock) -cne $profileSha256 -or
|
|
(Get-Sha256FromOpenStream $collectorLock) -cne $collectorSha256 -or
|
|
(Get-Sha256FromOpenStream $cliLock) -cne $cliSha256) {
|
|
Throw-ProfilePreflightError 'trusted_input_changed'
|
|
}
|
|
|
|
$passed = [bool]$data.onlineMetadataMatches
|
|
$report = [ordered]@{
|
|
schemaVersion = '1.0'
|
|
reportType = 'select_only_profile_preflight'
|
|
generatedAtUtc = [DateTime]::UtcNow.ToString(
|
|
'yyyy-MM-ddTHH:mm:ss.fffffffZ',
|
|
[Globalization.CultureInfo]::InvariantCulture)
|
|
passed = $passed
|
|
code = if ($passed) { 'ok' } else { 'catalog_metadata_mismatch' }
|
|
snapshotSha256 = $snapshotSha256
|
|
profileSha256 = $profileSha256
|
|
collectorSha256 = $collectorSha256
|
|
cliSha256 = $cliSha256
|
|
cliSignerThumbprint = $actualSignerThumbprint
|
|
cliResponseSha256 = Get-Sha256Bytes ($utf8.GetBytes($cliText))
|
|
catalogDatabaseScopeFingerprint = `
|
|
[string]$data.catalogDatabaseScopeFingerprint
|
|
permissionGateVerified = [bool]$data.permissionGateVerified
|
|
permissionRecheckVerified = [bool]$data.permissionRecheckVerified
|
|
safetyVerified = [bool]$data.safetyVerified
|
|
toolSourceBytesStableVerified = `
|
|
[bool]$data.toolSourceBytesStableVerified
|
|
databaseIdentityMatches = [bool]$data.databaseIdentityMatches
|
|
databaseMetadataMatches = [bool]$data.databaseMetadataMatches
|
|
criticalCatalogContractMatches = `
|
|
[bool]$data.criticalCatalogContractMatches
|
|
missingCriticalCatalogEntryCount = `
|
|
[int]$data.missingCriticalCatalogEntryCount
|
|
missingCriticalCatalogEntrySha256 = `
|
|
@($data.missingCriticalCatalogEntrySha256)
|
|
databaseSafety = [ordered]@{
|
|
applicationIntent = 'ReadOnly'
|
|
effectivePrincipalSelectOnly = $true
|
|
businessRowsRead = $false
|
|
storedProceduresExecuted = $false
|
|
writesAttempted = $false
|
|
}
|
|
registrationReady = $false
|
|
note = '本报告只证明 SELECT-only 目录与画像的当前匹配状态,不启用任何业务写命令。'
|
|
}
|
|
Publish-RestrictedReport $reportFull $report
|
|
$reportPublished = $true
|
|
[pscustomobject][ordered]@{
|
|
schemaVersion = '1.0'
|
|
passed = $passed
|
|
code = [string]$report.code
|
|
snapshotSha256 = $snapshotSha256
|
|
reportSha256 = (Get-FileHash -LiteralPath $reportFull `
|
|
-Algorithm SHA256).Hash.ToLowerInvariant()
|
|
reportPath = $reportFull
|
|
registrationReady = $false
|
|
}
|
|
if (-not $passed) {
|
|
Throw-ProfilePreflightError 'catalog_metadata_mismatch'
|
|
}
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_profile_preflight_failed:')) { throw }
|
|
Throw-ProfilePreflightError 'profile_preflight_failed'
|
|
}
|
|
finally {
|
|
$cliText = $null
|
|
if ($null -ne $cliLock) { $cliLock.Dispose() }
|
|
if ($null -ne $collectorLock) { $collectorLock.Dispose() }
|
|
if ($null -ne $profileLock) { $profileLock.Dispose() }
|
|
if (-not $reportPublished) {
|
|
if ($reportCreated -and $null -ne $reportFull -and
|
|
[IO.File]::Exists($reportFull)) {
|
|
try { [IO.File]::Delete($reportFull) } catch { }
|
|
}
|
|
if ($snapshotCreated -and $null -ne $snapshotFull -and
|
|
[IO.File]::Exists($snapshotFull)) {
|
|
try { [IO.File]::Delete($snapshotFull) } catch { }
|
|
}
|
|
}
|
|
}
|