754 lines
29 KiB
PowerShell
754 lines
29 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]$OutputPath,
|
|
|
|
[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_catalog_failed:windows_powershell_51_required'
|
|
}
|
|
|
|
$utf8 = [Text.UTF8Encoding]::new($false, $true)
|
|
$maximumCatalogEntries = 100000
|
|
$maximumOutputBytes = 16 * 1024 * 1024
|
|
$maximumToolBytes = 2 * 1024 * 1024
|
|
$toolPath = [string]$PSCommandPath
|
|
$connection = $null
|
|
$connectionBuilder = $null
|
|
$metadataCommand = $null
|
|
$metadataReader = $null
|
|
$outputFull = $null
|
|
$outputCreated = $false
|
|
$published = $false
|
|
|
|
function Throw-CatalogError([string]$Code) {
|
|
throw ('lserp_select_only_catalog_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-CatalogError $Code
|
|
}
|
|
$current = $current.Parent
|
|
}
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_catalog_failed:')) { throw }
|
|
Throw-CatalogError $Code
|
|
}
|
|
}
|
|
|
|
function Resolve-NewJsonPath([string]$Path) {
|
|
try {
|
|
$full = [IO.Path]::GetFullPath($Path)
|
|
if ([IO.Path]::GetExtension($full) -ine '.json' -or
|
|
[IO.File]::Exists($full) -or
|
|
[IO.Directory]::Exists($full)) {
|
|
Throw-CatalogError 'output_path_invalid'
|
|
}
|
|
$directory = [IO.Path]::GetDirectoryName($full)
|
|
if ([string]::IsNullOrWhiteSpace($directory)) {
|
|
Throw-CatalogError 'output_path_invalid'
|
|
}
|
|
Assert-NoReparseDirectoryChain $directory 'output_path_invalid'
|
|
return $full
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_catalog_failed:')) { throw }
|
|
Throw-CatalogError 'output_path_invalid'
|
|
}
|
|
}
|
|
|
|
function Assert-SafeServer([string]$Value) {
|
|
if (-not [Text.RegularExpressions.Regex]::IsMatch(
|
|
$Value,
|
|
'^[A-Za-z0-9][A-Za-z0-9.-]{0,252}(?:,[1-9][0-9]{0,4})?$')) {
|
|
Throw-CatalogError 'server_invalid'
|
|
}
|
|
$comma = $Value.LastIndexOf(',')
|
|
if ($comma -ge 0) {
|
|
$port = 0
|
|
if (-not [int]::TryParse($Value.Substring($comma + 1), [ref]$port) -or
|
|
$port -lt 1 -or $port -gt 65535) {
|
|
Throw-CatalogError 'server_invalid'
|
|
}
|
|
}
|
|
}
|
|
|
|
function Assert-SafeText(
|
|
[string]$Value,
|
|
[int]$MinimumLength,
|
|
[int]$MaximumLength,
|
|
[string]$Code
|
|
) {
|
|
if ($null -eq $Value -or
|
|
$Value.Length -lt $MinimumLength -or
|
|
$Value.Length -gt $MaximumLength -or
|
|
-not [string]::Equals(
|
|
$Value,
|
|
$Value.Trim(),
|
|
[StringComparison]::Ordinal)) {
|
|
Throw-CatalogError $Code
|
|
}
|
|
foreach ($character in $Value.ToCharArray()) {
|
|
if ([char]::IsControl($character)) { Throw-CatalogError $Code }
|
|
}
|
|
}
|
|
|
|
function Get-Sha256Text([string]$Value) {
|
|
$algorithm = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$bytes = $utf8.GetBytes($Value)
|
|
return ([BitConverter]::ToString(
|
|
$algorithm.ComputeHash($bytes))).Replace('-', '').ToLowerInvariant()
|
|
}
|
|
finally {
|
|
$algorithm.Dispose()
|
|
}
|
|
}
|
|
|
|
function Read-Int64(
|
|
[Data.Common.DbDataReader]$Reader,
|
|
[string]$Name,
|
|
[long]$Minimum,
|
|
[long]$Maximum
|
|
) {
|
|
try {
|
|
$ordinal = $Reader.GetOrdinal($Name)
|
|
if ($Reader.IsDBNull($ordinal)) {
|
|
Throw-CatalogError 'query_contract_invalid'
|
|
}
|
|
$value = [Convert]::ToInt64(
|
|
$Reader.GetValue($ordinal),
|
|
[Globalization.CultureInfo]::InvariantCulture)
|
|
if ($value -lt $Minimum -or $value -gt $Maximum) {
|
|
Throw-CatalogError 'query_contract_invalid'
|
|
}
|
|
return $value
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_catalog_failed:')) { throw }
|
|
Throw-CatalogError 'query_contract_invalid'
|
|
}
|
|
}
|
|
|
|
function Read-Text(
|
|
[Data.Common.DbDataReader]$Reader,
|
|
[string]$Name,
|
|
[int]$MinimumLength,
|
|
[int]$MaximumLength
|
|
) {
|
|
try {
|
|
$ordinal = $Reader.GetOrdinal($Name)
|
|
if ($Reader.IsDBNull($ordinal)) {
|
|
Throw-CatalogError 'query_contract_invalid'
|
|
}
|
|
$value = [Convert]::ToString(
|
|
$Reader.GetValue($ordinal),
|
|
[Globalization.CultureInfo]::InvariantCulture)
|
|
Assert-SafeText $value $MinimumLength $MaximumLength `
|
|
'query_contract_invalid'
|
|
return $value
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_catalog_failed:')) { throw }
|
|
Throw-CatalogError 'query_contract_invalid'
|
|
}
|
|
}
|
|
|
|
function Get-CurrentToolFileSha256([string]$Path) {
|
|
try {
|
|
if ([string]::IsNullOrWhiteSpace($Path)) {
|
|
Throw-CatalogError 'tool_source_invalid'
|
|
}
|
|
$full = [IO.Path]::GetFullPath($Path)
|
|
$item = Get-Item -LiteralPath $full -Force
|
|
if ($item.PSIsContainer -or
|
|
$item.Length -le 0 -or
|
|
$item.Length -gt $maximumToolBytes -or
|
|
(($item.Attributes -band
|
|
[IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
Throw-CatalogError 'tool_source_invalid'
|
|
}
|
|
Assert-NoReparseDirectoryChain $item.DirectoryName `
|
|
'tool_source_invalid'
|
|
$hash = (Get-FileHash -LiteralPath $full `
|
|
-Algorithm SHA256).Hash.ToLowerInvariant()
|
|
if ($hash -cnotmatch '^[0-9a-f]{64}$') {
|
|
Throw-CatalogError 'tool_source_invalid'
|
|
}
|
|
return $hash
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_catalog_failed:')) { throw }
|
|
Throw-CatalogError 'tool_source_invalid'
|
|
}
|
|
}
|
|
|
|
function Assert-SelectOnlyPermissionGate(
|
|
[Data.Common.DbConnection]$connection,
|
|
[string]$permissionQuery,
|
|
[int]$commandTimeoutSeconds
|
|
) {
|
|
$permissionCommand = $null
|
|
$permissionReader = $null
|
|
try {
|
|
$permissionCommand = $connection.CreateCommand()
|
|
$permissionCommand.CommandType = [Data.CommandType]::Text
|
|
$permissionCommand.CommandText = $permissionQuery
|
|
$permissionCommand.CommandTimeout = $commandTimeoutSeconds
|
|
$permissionReader = $permissionCommand.ExecuteReader(
|
|
[Data.CommandBehavior]::SingleResult)
|
|
if (-not $permissionReader.Read()) {
|
|
Throw-CatalogError 'permission_contract_invalid'
|
|
}
|
|
$permissionColumns = @(
|
|
'database_control', 'database_alter', 'database_insert',
|
|
'database_update', 'database_delete', 'database_execute',
|
|
'database_create_table', 'database_create_procedure',
|
|
'database_create_view', 'database_create_function',
|
|
'server_control', 'server_alter_login', 'server_impersonate_login',
|
|
'sysadmin_member', 'db_owner_member', 'db_ddladmin_member',
|
|
'db_datawriter_member', 'writable_object_count',
|
|
'executable_object_count', 'writable_schema_count'
|
|
)
|
|
foreach ($column in $permissionColumns) {
|
|
if ((Read-Int64 $permissionReader $column 0 10000000) -ne 0) {
|
|
Throw-CatalogError 'database_principal_not_select_only'
|
|
}
|
|
}
|
|
if ($permissionReader.Read() -or $permissionReader.NextResult()) {
|
|
Throw-CatalogError 'permission_contract_invalid'
|
|
}
|
|
}
|
|
finally {
|
|
if ($null -ne $permissionReader) { $permissionReader.Dispose() }
|
|
if ($null -ne $permissionCommand) { $permissionCommand.Dispose() }
|
|
}
|
|
}
|
|
|
|
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-CatalogError 'output_acl_invalid'
|
|
}
|
|
}
|
|
|
|
function Publish-RestrictedJson([string]$Path, [string]$Json) {
|
|
$stream = $null
|
|
$writer = $null
|
|
try {
|
|
$bytes = $utf8.GetByteCount($Json)
|
|
if ($bytes -le 0 -or $bytes -gt $maximumOutputBytes) {
|
|
Throw-CatalogError 'output_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:outputCreated = $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
|
|
if ($item.Length -le 0 -or $item.Length -gt $maximumOutputBytes -or
|
|
(($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
|
|
Throw-CatalogError 'output_publish_invalid'
|
|
}
|
|
$acl = [IO.File]::GetAccessControl($Path)
|
|
if (-not $acl.AreAccessRulesProtected) {
|
|
Throw-CatalogError 'output_acl_invalid'
|
|
}
|
|
}
|
|
finally {
|
|
if ($null -ne $writer) { $writer.Dispose() }
|
|
if ($null -ne $stream) { $stream.Dispose() }
|
|
}
|
|
}
|
|
|
|
$permissionQuery = @'
|
|
SELECT
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'CONTROL'), 0)) AS database_control,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'ALTER'), 0)) AS database_alter,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'INSERT'), 0)) AS database_insert,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'UPDATE'), 0)) AS database_update,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'DELETE'), 0)) AS database_delete,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'EXECUTE'), 0)) AS database_execute,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'CREATE TABLE'), 0)) AS database_create_table,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'CREATE PROCEDURE'), 0)) AS database_create_procedure,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'CREATE VIEW'), 0)) AS database_create_view,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), 'DATABASE', 'CREATE FUNCTION'), 0)) AS database_create_function,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(NULL, NULL, 'CONTROL SERVER'), 0)) AS server_control,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(NULL, NULL, 'ALTER ANY LOGIN'), 0)) AS server_alter_login,
|
|
CONVERT(int, ISNULL(HAS_PERMS_BY_NAME(NULL, NULL, 'IMPERSONATE ANY LOGIN'), 0)) AS server_impersonate_login,
|
|
CONVERT(int, ISNULL(IS_SRVROLEMEMBER('sysadmin'), 0)) AS sysadmin_member,
|
|
CONVERT(int, ISNULL(IS_MEMBER('db_owner'), 0)) AS db_owner_member,
|
|
CONVERT(int, ISNULL(IS_MEMBER('db_ddladmin'), 0)) AS db_ddladmin_member,
|
|
CONVERT(int, ISNULL(IS_MEMBER('db_datawriter'), 0)) AS db_datawriter_member,
|
|
CONVERT(bigint,
|
|
(
|
|
SELECT COUNT_BIG(1)
|
|
FROM sys.objects AS writable_object
|
|
WHERE writable_object.is_ms_shipped = 0
|
|
AND writable_object.type IN ('U', 'V')
|
|
AND
|
|
(
|
|
ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(SCHEMA_NAME(writable_object.schema_id)) + N'.' +
|
|
QUOTENAME(writable_object.name), 'OBJECT', 'INSERT'), 0) = 1
|
|
OR ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(SCHEMA_NAME(writable_object.schema_id)) + N'.' +
|
|
QUOTENAME(writable_object.name), 'OBJECT', 'UPDATE'), 0) = 1
|
|
OR ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(SCHEMA_NAME(writable_object.schema_id)) + N'.' +
|
|
QUOTENAME(writable_object.name), 'OBJECT', 'DELETE'), 0) = 1
|
|
OR ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(SCHEMA_NAME(writable_object.schema_id)) + N'.' +
|
|
QUOTENAME(writable_object.name), 'OBJECT', 'ALTER'), 0) = 1
|
|
OR ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(SCHEMA_NAME(writable_object.schema_id)) + N'.' +
|
|
QUOTENAME(writable_object.name), 'OBJECT', 'CONTROL'), 0) = 1
|
|
)
|
|
)) AS writable_object_count,
|
|
CONVERT(bigint,
|
|
(
|
|
SELECT COUNT_BIG(1)
|
|
FROM sys.objects AS executable_object
|
|
WHERE executable_object.is_ms_shipped = 0
|
|
AND executable_object.type IN ('P', 'PC', 'FN', 'IF', 'TF')
|
|
AND
|
|
(
|
|
ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(SCHEMA_NAME(executable_object.schema_id)) + N'.' +
|
|
QUOTENAME(executable_object.name), 'OBJECT', 'EXECUTE'), 0) = 1
|
|
OR ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(SCHEMA_NAME(executable_object.schema_id)) + N'.' +
|
|
QUOTENAME(executable_object.name), 'OBJECT', 'ALTER'), 0) = 1
|
|
OR ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(SCHEMA_NAME(executable_object.schema_id)) + N'.' +
|
|
QUOTENAME(executable_object.name), 'OBJECT', 'CONTROL'), 0) = 1
|
|
)
|
|
)) AS executable_object_count,
|
|
CONVERT(bigint,
|
|
(
|
|
SELECT COUNT_BIG(1)
|
|
FROM sys.schemas AS writable_schema
|
|
WHERE writable_schema.name NOT IN (N'sys', N'INFORMATION_SCHEMA')
|
|
AND
|
|
(
|
|
ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(writable_schema.name), 'SCHEMA', 'ALTER'), 0) = 1
|
|
OR ISNULL(HAS_PERMS_BY_NAME(
|
|
QUOTENAME(writable_schema.name), 'SCHEMA', 'CONTROL'), 0) = 1
|
|
)
|
|
)) AS writable_schema_count;
|
|
'@
|
|
|
|
$metadataQuery = @'
|
|
SELECT
|
|
CONVERT(nvarchar(128), DB_NAME()) AS database_name,
|
|
CONVERT(nvarchar(128), SERVERPROPERTY('ServerName')) AS actual_server_name,
|
|
CONVERT(nvarchar(128), SUSER_SNAME()) AS effective_principal,
|
|
CONVERT(int, SERVERPROPERTY('ProductMajorVersion')) AS sql_server_major_version,
|
|
CONVERT(int, current_database.compatibility_level) AS compatibility_level,
|
|
CONVERT(bigint, (SELECT COUNT_BIG(1) FROM sys.tables WHERE is_ms_shipped = 0)) AS user_table_count,
|
|
CONVERT(bigint, (SELECT COUNT_BIG(1) FROM sys.views WHERE is_ms_shipped = 0)) AS user_view_count,
|
|
CONVERT(bigint, (SELECT COUNT_BIG(1) FROM sys.procedures WHERE is_ms_shipped = 0)) AS user_procedure_count,
|
|
CONVERT(bigint, (SELECT COUNT_BIG(1) FROM sys.triggers WHERE is_ms_shipped = 0)) AS user_trigger_count,
|
|
CONVERT(bigint,
|
|
(
|
|
SELECT COUNT_BIG(1)
|
|
FROM sys.objects
|
|
WHERE schema_id = SCHEMA_ID(N'dbo')
|
|
AND
|
|
(
|
|
(type = 'U' AND name IN
|
|
(
|
|
N'p_agent_business_audit',
|
|
N'p_agent_business_source_document',
|
|
N'p_agent_command_idempotency',
|
|
N'p_agent_integration_outbox',
|
|
N'p_agent_purchase_currency_crosswalk',
|
|
N'p_agent_purchase_row_scope',
|
|
N'p_agent_workflow_adapter_evidence',
|
|
N'p_agent_workflow_adapter_evidence_v2'
|
|
))
|
|
OR
|
|
(type = 'P' AND name IN
|
|
(
|
|
N'p_lserp_agent_workflow_read',
|
|
N'p_lserp_agent_workflow_read_compat100',
|
|
N'p_lserp_agent_workflow_readiness',
|
|
N'p_lserp_agent_workflow_readiness_v2',
|
|
N'p_lserp_agent_workflow_readiness_v3',
|
|
N'p_lserp_agent_workflow_write',
|
|
N'p_lserp_agent_workflow_write_leave_compat100',
|
|
N'p_lserp_agent_workflow_write_purchase_compat100'
|
|
))
|
|
)
|
|
)) AS agent_workflow_object_count
|
|
FROM sys.databases AS current_database
|
|
WHERE current_database.database_id = DB_ID();
|
|
|
|
SELECT TOP (100001)
|
|
catalog_entry.entry_kind,
|
|
catalog_entry.schema_name,
|
|
catalog_entry.object_name,
|
|
catalog_entry.object_kind,
|
|
catalog_entry.member_name
|
|
FROM
|
|
(
|
|
SELECT
|
|
CONVERT(varchar(16), 'object') AS entry_kind,
|
|
CONVERT(nvarchar(128), SCHEMA_NAME(catalog_object.schema_id)) AS schema_name,
|
|
CONVERT(nvarchar(128), catalog_object.name) AS object_name,
|
|
CONVERT(varchar(16), CASE catalog_object.type
|
|
WHEN 'U' THEN 'table'
|
|
WHEN 'V' THEN 'view'
|
|
WHEN 'P' THEN 'procedure'
|
|
ELSE 'invalid' END) AS object_kind,
|
|
CONVERT(nvarchar(128), N'') AS member_name
|
|
FROM sys.objects AS catalog_object
|
|
WHERE catalog_object.is_ms_shipped = 0
|
|
AND catalog_object.type IN ('U', 'V', 'P')
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
CONVERT(varchar(16), 'column'),
|
|
CONVERT(nvarchar(128), SCHEMA_NAME(catalog_object.schema_id)),
|
|
CONVERT(nvarchar(128), catalog_object.name),
|
|
CONVERT(varchar(16), CASE catalog_object.type
|
|
WHEN 'U' THEN 'table'
|
|
WHEN 'V' THEN 'view'
|
|
ELSE 'invalid' END),
|
|
CONVERT(nvarchar(128), catalog_column.name)
|
|
FROM sys.objects AS catalog_object
|
|
INNER JOIN sys.columns AS catalog_column
|
|
ON catalog_column.object_id = catalog_object.object_id
|
|
WHERE catalog_object.is_ms_shipped = 0
|
|
AND catalog_object.type IN ('U', 'V')
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
CONVERT(varchar(16), 'parameter'),
|
|
CONVERT(nvarchar(128), SCHEMA_NAME(catalog_procedure.schema_id)),
|
|
CONVERT(nvarchar(128), catalog_procedure.name),
|
|
CONVERT(varchar(16), 'procedure'),
|
|
CONVERT(nvarchar(128), catalog_parameter.name)
|
|
FROM sys.procedures AS catalog_procedure
|
|
INNER JOIN sys.parameters AS catalog_parameter
|
|
ON catalog_parameter.object_id = catalog_procedure.object_id
|
|
AND catalog_parameter.parameter_id > 0
|
|
WHERE catalog_procedure.is_ms_shipped = 0
|
|
) AS catalog_entry
|
|
ORDER BY
|
|
catalog_entry.object_kind,
|
|
catalog_entry.schema_name,
|
|
catalog_entry.object_name,
|
|
catalog_entry.entry_kind,
|
|
catalog_entry.member_name;
|
|
'@
|
|
|
|
try {
|
|
Assert-SafeServer $Server
|
|
$outputFull = Resolve-NewJsonPath $OutputPath
|
|
$executedScriptText = $null
|
|
try {
|
|
$executedScriptText = [string]
|
|
$MyInvocation.MyCommand.ScriptBlock.Ast.Extent.Text
|
|
}
|
|
catch {
|
|
Throw-CatalogError 'tool_source_invalid'
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($executedScriptText) -or
|
|
$utf8.GetByteCount($executedScriptText) -gt $maximumToolBytes) {
|
|
Throw-CatalogError 'tool_source_invalid'
|
|
}
|
|
$executedToolSha256 = Get-Sha256Text $executedScriptText
|
|
$initialToolFileSha256 = Get-CurrentToolFileSha256 $toolPath
|
|
if ($initialToolFileSha256 -cne $executedToolSha256) {
|
|
Throw-CatalogError 'tool_source_changed'
|
|
}
|
|
if ($null -ne $Credential) {
|
|
Assert-SafeText $Credential.UserName 1 128 'credential_user_invalid'
|
|
}
|
|
|
|
$connectionBuilder = [System.Data.SqlClient.SqlConnectionStringBuilder]::new()
|
|
$connectionBuilder.DataSource = $Server
|
|
$connectionBuilder.InitialCatalog = $Database
|
|
$connectionBuilder.ConnectTimeout = $ConnectionTimeoutSeconds
|
|
$connectionBuilder.Encrypt = $true
|
|
$connectionBuilder.TrustServerCertificate = $false
|
|
$connectionBuilder.ApplicationIntent =
|
|
[System.Data.SqlClient.ApplicationIntent]::ReadOnly
|
|
$connectionBuilder.PersistSecurityInfo = $false
|
|
$connectionBuilder.Pooling = $false
|
|
$connectionBuilder.MultipleActiveResultSets = $false
|
|
$connectionBuilder.ApplicationName =
|
|
'Langsu Lserp SELECT-only catalog snapshot'
|
|
if ($PSCmdlet.ParameterSetName -eq 'WindowsCredential') {
|
|
$connectionBuilder.IntegratedSecurity = $true
|
|
}
|
|
else {
|
|
$connectionBuilder.IntegratedSecurity = $false
|
|
$connectionBuilder.UserID = $Credential.UserName
|
|
$connectionBuilder.Password = $Credential.GetNetworkCredential().Password
|
|
}
|
|
|
|
$connection = [System.Data.SqlClient.SqlConnection]::new(
|
|
$connectionBuilder.ConnectionString)
|
|
$connection.Open()
|
|
|
|
Assert-SelectOnlyPermissionGate `
|
|
$connection $permissionQuery $CommandTimeoutSeconds
|
|
|
|
$metadataCommand = $connection.CreateCommand()
|
|
$metadataCommand.CommandType = [Data.CommandType]::Text
|
|
$metadataCommand.CommandText = $metadataQuery
|
|
$metadataCommand.CommandTimeout = $CommandTimeoutSeconds
|
|
$metadataReader = $metadataCommand.ExecuteReader(
|
|
[Data.CommandBehavior]::SequentialAccess)
|
|
if (-not $metadataReader.Read()) {
|
|
Throw-CatalogError 'metadata_contract_invalid'
|
|
}
|
|
$actualDatabase = Read-Text $metadataReader 'database_name' 1 128
|
|
$actualServer = Read-Text $metadataReader 'actual_server_name' 1 128
|
|
$effectivePrincipal = Read-Text $metadataReader 'effective_principal' 1 128
|
|
if (-not [string]::Equals(
|
|
$actualDatabase,
|
|
$Database,
|
|
[StringComparison]::OrdinalIgnoreCase)) {
|
|
Throw-CatalogError 'database_identity_mismatch'
|
|
}
|
|
$sqlServerMajorVersion = Read-Int64 `
|
|
$metadataReader 'sql_server_major_version' 9 99
|
|
$compatibilityLevel = Read-Int64 `
|
|
$metadataReader 'compatibility_level' 80 200
|
|
$userTableCount = Read-Int64 `
|
|
$metadataReader 'user_table_count' 0 10000000
|
|
$userViewCount = Read-Int64 `
|
|
$metadataReader 'user_view_count' 0 10000000
|
|
$userProcedureCount = Read-Int64 `
|
|
$metadataReader 'user_procedure_count' 0 10000000
|
|
$userTriggerCount = Read-Int64 `
|
|
$metadataReader 'user_trigger_count' 0 10000000
|
|
$agentWorkflowObjectCount = Read-Int64 `
|
|
$metadataReader 'agent_workflow_object_count' 0 16
|
|
if ($metadataReader.Read() -or -not $metadataReader.NextResult()) {
|
|
Throw-CatalogError 'metadata_contract_invalid'
|
|
}
|
|
|
|
$catalogHashSet = [Collections.Generic.HashSet[string]]::new(
|
|
[StringComparer]::Ordinal)
|
|
while ($metadataReader.Read()) {
|
|
if ($catalogHashSet.Count -ge $maximumCatalogEntries) {
|
|
Throw-CatalogError 'catalog_too_large'
|
|
}
|
|
$entryKind = Read-Text $metadataReader 'entry_kind' 1 16
|
|
$schemaName = Read-Text $metadataReader 'schema_name' 1 128
|
|
$objectName = Read-Text $metadataReader 'object_name' 1 128
|
|
$objectKind = Read-Text $metadataReader 'object_kind' 1 16
|
|
$memberMinimum = if ($entryKind -eq 'object') { 0 } else { 1 }
|
|
$memberName = Read-Text `
|
|
$metadataReader 'member_name' $memberMinimum 128
|
|
if ($entryKind -cnotin @('object', 'column', 'parameter') -or
|
|
$objectKind -cnotin @('table', 'view', 'procedure') -or
|
|
($entryKind -eq 'object' -and $memberName.Length -ne 0) -or
|
|
($entryKind -eq 'column' -and $objectKind -eq 'procedure') -or
|
|
($entryKind -eq 'parameter' -and $objectKind -ne 'procedure')) {
|
|
Throw-CatalogError 'catalog_contract_invalid'
|
|
}
|
|
$catalogKey = [string]::Join(
|
|
[char]0x1f,
|
|
@(
|
|
$entryKind,
|
|
$schemaName.ToLowerInvariant(),
|
|
$objectName.ToLowerInvariant(),
|
|
$objectKind,
|
|
$memberName.ToLowerInvariant()
|
|
))
|
|
$entryHash = Get-Sha256Text $catalogKey
|
|
if (-not $catalogHashSet.Add($entryHash)) {
|
|
Throw-CatalogError 'catalog_contract_invalid'
|
|
}
|
|
}
|
|
if ($catalogHashSet.Count -le 0 -or $metadataReader.NextResult()) {
|
|
Throw-CatalogError 'metadata_contract_invalid'
|
|
}
|
|
$metadataReader.Dispose()
|
|
$metadataReader = $null
|
|
|
|
Assert-SelectOnlyPermissionGate `
|
|
$connection $permissionQuery $CommandTimeoutSeconds
|
|
$finalToolFileSha256 = Get-CurrentToolFileSha256 $toolPath
|
|
if ($finalToolFileSha256 -cne $initialToolFileSha256 -or
|
|
$finalToolFileSha256 -cne $executedToolSha256) {
|
|
Throw-CatalogError 'tool_source_changed'
|
|
}
|
|
|
|
[string[]]$catalogHashes = @($catalogHashSet)
|
|
[Array]::Sort($catalogHashes, [StringComparer]::Ordinal)
|
|
$catalogSetSha256 = Get-Sha256Text(
|
|
[string]::Join("`n", $catalogHashes))
|
|
$toolSha256 = $executedToolSha256
|
|
$databaseScopeFingerprint = Get-Sha256Text([string]::Join(
|
|
"`n",
|
|
@(
|
|
'sqlserver-select-only-catalog-v1',
|
|
$Server.ToLowerInvariant(),
|
|
$actualServer.ToLowerInvariant(),
|
|
$actualDatabase.ToLowerInvariant(),
|
|
$effectivePrincipal.ToLowerInvariant()
|
|
)))
|
|
$snapshot = [ordered]@{
|
|
schemaVersion = '1.1'
|
|
snapshotType = 'select_only_sqlserver_catalog_hashes'
|
|
generatedAtUtc = [DateTime]::UtcNow.ToString(
|
|
'yyyy-MM-ddTHH:mm:ss.fffffffZ',
|
|
[Globalization.CultureInfo]::InvariantCulture)
|
|
toolSha256 = $toolSha256
|
|
databaseScopeFingerprint = $databaseScopeFingerprint
|
|
identity = [ordered]@{
|
|
requestedServerSha256 = Get-Sha256Text ($Server.ToLowerInvariant())
|
|
actualServerSha256 = Get-Sha256Text ($actualServer.ToLowerInvariant())
|
|
databaseNameSha256 = Get-Sha256Text ($actualDatabase.ToLowerInvariant())
|
|
effectivePrincipalSha256 = Get-Sha256Text `
|
|
($effectivePrincipal.ToLowerInvariant())
|
|
}
|
|
database = [ordered]@{
|
|
sqlServerMajorVersion = $sqlServerMajorVersion
|
|
compatibilityLevel = $compatibilityLevel
|
|
userTableCount = $userTableCount
|
|
userViewCount = $userViewCount
|
|
userProcedureCount = $userProcedureCount
|
|
userTriggerCount = $userTriggerCount
|
|
agentWorkflowObjectCount = $agentWorkflowObjectCount
|
|
agentWorkflowObjectsPresent = ($agentWorkflowObjectCount -eq 16)
|
|
}
|
|
permissionGate = [ordered]@{
|
|
passed = $true
|
|
checkedBeforeCatalogRead = $true
|
|
checkedAfterCatalogRead = $true
|
|
databaseWritePermissionCount = 0
|
|
serverWritePermissionCount = 0
|
|
writableObjectCount = 0
|
|
executableObjectCount = 0
|
|
writableSchemaCount = 0
|
|
}
|
|
catalog = [ordered]@{
|
|
canonicalization = 'lower_invariant_unit_separator_v1'
|
|
entryCount = $catalogHashes.Count
|
|
setSha256 = $catalogSetSha256
|
|
entrySha256 = $catalogHashes
|
|
}
|
|
safety = [ordered]@{
|
|
connectionEncrypted = $true
|
|
serverCertificateValidated = $true
|
|
applicationIntent = 'ReadOnly'
|
|
effectivePrincipalSelectOnly = $true
|
|
toolSourceBytesStable = $true
|
|
businessRowsRead = $false
|
|
storedProceduresExecuted = $false
|
|
writesAttempted = $false
|
|
}
|
|
}
|
|
$json = ConvertTo-Json -InputObject $snapshot -Depth 8 -Compress
|
|
Publish-RestrictedJson $outputFull $json
|
|
$published = $true
|
|
$snapshotSha256 = (Get-FileHash -LiteralPath $outputFull `
|
|
-Algorithm SHA256).Hash.ToLowerInvariant()
|
|
[pscustomobject][ordered]@{
|
|
schemaVersion = '1.1'
|
|
passed = $true
|
|
code = 'ok'
|
|
outputPath = $outputFull
|
|
snapshotSha256 = $snapshotSha256
|
|
databaseScopeFingerprint = $databaseScopeFingerprint
|
|
catalogEntryCount = $catalogHashes.Count
|
|
catalogSetSha256 = $catalogSetSha256
|
|
}
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message.StartsWith(
|
|
'lserp_select_only_catalog_failed:')) { throw }
|
|
Throw-CatalogError 'catalog_snapshot_failed'
|
|
}
|
|
finally {
|
|
if ($null -ne $metadataReader) { $metadataReader.Dispose() }
|
|
if ($null -ne $metadataCommand) { $metadataCommand.Dispose() }
|
|
if ($null -ne $connection) {
|
|
try { $connection.Close() } catch { }
|
|
$connection.Dispose()
|
|
}
|
|
if ($null -ne $connectionBuilder) {
|
|
$connectionBuilder.Password = [string]::Empty
|
|
$connectionBuilder.UserID = [string]::Empty
|
|
}
|
|
if ($outputCreated -and -not $published -and
|
|
$null -ne $outputFull -and [IO.File]::Exists($outputFull)) {
|
|
try { [IO.File]::Delete($outputFull) } catch { }
|
|
}
|
|
}
|