Private/SiemProfileStore.ps1

$script:SiemProfileStoreRoot = $null

function Assert-SiemProfileName {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    if ($Name -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$') {
        throw 'Profile name must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$.'
    }
}

function Get-SiemProfileDirectory {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope
    )

    $override = $script:SiemProfileStoreRoot
    if ([string]::IsNullOrWhiteSpace($override)) {
        $override = $env:WSA_SIEM_PROFILE_ROOT
    }
    if (-not [string]::IsNullOrWhiteSpace($override)) {
        return (Join-Path -Path $override -ChildPath $Scope)
    }

    if ($Scope -eq 'LocalMachine') {
        $base = [Environment]::GetFolderPath([Environment+SpecialFolder]::CommonApplicationData)
        if ([string]::IsNullOrWhiteSpace($base)) { $base = $env:ProgramData }
    }
    else {
        $base = [Environment]::GetFolderPath([Environment+SpecialFolder]::ApplicationData)
        if ([string]::IsNullOrWhiteSpace($base)) { $base = $env:APPDATA }
    }
    if ([string]::IsNullOrWhiteSpace($base)) {
        throw 'The profile storage root is unavailable on this platform. Set WSA_SIEM_PROFILE_ROOT for testing.'
    }
    return (Join-Path -Path (Join-Path -Path $base -ChildPath 'WindowsSecurityAudit') -ChildPath 'SiemProfiles')
}

function Get-SiemProfilePath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope
    )

    Assert-SiemProfileName -Name $Name
    return (Join-Path -Path (Get-SiemProfileDirectory -Scope $Scope) -ChildPath ($Name + '.json'))
}

function Protect-SiemDpapiText {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$PlainText,

        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope,

        [Parameter(Mandatory = $true)]
        [string]$Context
    )

    if ($null -eq ('System.Security.Cryptography.ProtectedData' -as [type])) {
        Add-Type -AssemblyName System.Security -ErrorAction Stop
    }

    $plain = $null
    $entropy = $null
    $cipher = $null
    try {
        $plain = [Text.Encoding]::UTF8.GetBytes($PlainText)
        $entropy = [Text.Encoding]::UTF8.GetBytes($Context)
        $protectionScope = [Enum]::Parse([Security.Cryptography.DataProtectionScope], $Scope)
        $cipher = [Security.Cryptography.ProtectedData]::Protect($plain, $entropy, $protectionScope)
        $prefix = $(if ($Scope -eq 'LocalMachine') { 'wsa-dpapi-lm-v1:' } else { 'wsa-dpapi-cu-v1:' })
        return ($prefix + [Convert]::ToBase64String($cipher))
    }
    catch {
        throw 'DPAPI protection failed. Profiles require Windows DPAPI and cannot fall back to plaintext.'
    }
    finally {
        if ($null -ne $plain) { [Array]::Clear($plain, 0, $plain.Length) }
        if ($null -ne $entropy) { [Array]::Clear($entropy, 0, $entropy.Length) }
        if ($null -ne $cipher) { [Array]::Clear($cipher, 0, $cipher.Length) }
    }
}

function Protect-SiemSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [SecureString]$Secret,

        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope,

        [Parameter()]
        [ValidateSet('Secret', 'Uri')]
        [string]$Purpose = 'Secret'
    )

    Assert-SiemProfileName -Name $Name
    $plainText = $null
    try {
        $plainText = ConvertFrom-SiemSecureString -SecureString $Secret
        $context = 'WindowsSecurityAudit/SiemProfile/{0}/{1}/{2}/v1' -f $Scope, $Name, $Purpose
        return Protect-SiemDpapiText -PlainText $plainText -Scope $Scope -Context $context
    }
    finally {
        $plainText = $null
    }
}

function Unprotect-SiemSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SecretRef,

        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope,

        [Parameter()]
        [ValidateSet('Secret', 'Uri')]
        [string]$Purpose = 'Secret'
    )

    Assert-SiemProfileName -Name $Name
    $prefix = $(if ($Scope -eq 'LocalMachine') { 'wsa-dpapi-lm-v1:' } else { 'wsa-dpapi-cu-v1:' })
    if (-not $SecretRef.StartsWith($prefix, [StringComparison]::Ordinal)) {
        throw 'The protected value has an invalid marker or belongs to a different DPAPI scope.'
    }
    if ($null -eq ('System.Security.Cryptography.ProtectedData' -as [type])) {
        Add-Type -AssemblyName System.Security -ErrorAction Stop
    }

    $plain = $null
    $entropy = $null
    $cipher = $null
    try {
        $cipher = [Convert]::FromBase64String($SecretRef.Substring($prefix.Length))
        $context = 'WindowsSecurityAudit/SiemProfile/{0}/{1}/{2}/v1' -f $Scope, $Name, $Purpose
        $entropy = [Text.Encoding]::UTF8.GetBytes($context)
        $protectionScope = [Enum]::Parse([Security.Cryptography.DataProtectionScope], $Scope)
        $plain = [Security.Cryptography.ProtectedData]::Unprotect($cipher, $entropy, $protectionScope)
        return [Text.Encoding]::UTF8.GetString($plain)
    }
    catch {
        throw 'DPAPI unprotection failed. Verify the profile scope, profile name, machine, and service account.'
    }
    finally {
        if ($null -ne $plain) { [Array]::Clear($plain, 0, $plain.Length) }
        if ($null -ne $entropy) { [Array]::Clear($entropy, 0, $entropy.Length) }
        if ($null -ne $cipher) { [Array]::Clear($cipher, 0, $cipher.Length) }
    }
}

function ConvertTo-SiemSidString {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Identity
    )

    try {
        $sid = New-Object System.Security.Principal.SecurityIdentifier($Identity)
        return $sid.Value
    }
    catch {
        $account = New-Object System.Security.Principal.NTAccount($Identity)
        return $account.Translate([Security.Principal.SecurityIdentifier]).Value
    }
}

function Protect-SiemProfileAcl {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope
    )

    if ($env:OS -ne 'Windows_NT') {
        Write-Verbose "Non-Windows host: skipping profile ACL lockdown for '$Path'."
        return
    }

    $identities = @()
    $ownerSid = $null
    if ($Scope -eq 'LocalMachine') {
        $identities = @('S-1-5-18', 'S-1-5-32-544')
        $ownerSid = 'S-1-5-32-544'
    }
    else {
        $currentSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
        $identities = @($currentSid)
        $ownerSid = $currentSid
    }

    $acl = Get-Acl -Path $Path -ErrorAction Stop
    $acl.SetAccessRuleProtection($true, $false)
    foreach ($existingRule in @($acl.Access)) {
        $null = $acl.RemoveAccessRule($existingRule)
    }
    foreach ($identity in $identities) {
        $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
            (New-Object System.Security.Principal.SecurityIdentifier($identity)),
            [Security.AccessControl.FileSystemRights]::FullControl,
            [Security.AccessControl.AccessControlType]::Allow)
        $acl.AddAccessRule($accessRule)
    }
    $acl.SetOwner((New-Object System.Security.Principal.SecurityIdentifier($ownerSid)))
    Set-Acl -Path $Path -AclObject $acl -ErrorAction Stop

    $applied = Get-Acl -Path $Path -ErrorAction Stop
    if (-not $applied.AreAccessRulesProtected) {
        throw 'Profile ACL verification failed: inheritance is still enabled.'
    }
    if ((ConvertTo-SiemSidString -Identity $applied.Owner) -ne $ownerSid) {
        throw 'Profile ACL verification failed: unexpected file owner.'
    }
    $rules = @($applied.Access)
    if ($rules.Count -ne $identities.Count) {
        throw 'Profile ACL verification failed: unexpected access rules remain.'
    }
    foreach ($rule in $rules) {
        $ruleSid = ConvertTo-SiemSidString -Identity $rule.IdentityReference.Value
        if ($identities -notcontains $ruleSid -or
            $rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow -or
            $rule.FileSystemRights -ne [Security.AccessControl.FileSystemRights]::FullControl) {
            throw 'Profile ACL verification failed: an access rule is not restricted to the required identity.'
        }
    }
}

function Initialize-SiemProfileDirectory {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope
    )

    $directory = Get-SiemProfileDirectory -Scope $Scope
    if (-not (Test-Path -LiteralPath $directory -PathType Container)) {
        New-Item -Path $directory -ItemType Directory -Force -ErrorAction Stop | Out-Null
    }
    Protect-SiemProfileAcl -Path $directory -Scope $Scope
    return $directory
}

function Move-SiemProfileFile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$TemporaryPath,

        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter()]
        [switch]$Replace
    )

    if ($Replace.IsPresent) {
        if ($env:OS -eq 'Windows_NT') {
            [IO.File]::Replace($TemporaryPath, $Path, [NullString]::Value)
        }
        else {
            [IO.File]::Move($TemporaryPath, $Path, $true)
        }
    }
    else {
        [IO.File]::Move($TemporaryPath, $Path)
    }
}

function Write-SiemProfileJson {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$Profile,

        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope,

        [Parameter()]
        [switch]$Replace
    )

    $directory = Initialize-SiemProfileDirectory -Scope $Scope
    $targetExists = Test-Path -LiteralPath $Path -PathType Leaf
    if ($targetExists -and -not $Replace.IsPresent) {
        throw "SIEM profile already exists. Use -Force to replace it."
    }
    if ($targetExists) {
        Protect-SiemProfileAcl -Path $Path -Scope $Scope
    }

    $temporaryPath = Join-Path -Path $directory -ChildPath ('.' + $Profile.name + '.' + [Guid]::NewGuid().ToString('N') + '.tmp')
    $stream = $null
    try {
        $stream = New-Object System.IO.FileStream($temporaryPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
        $stream.Close()
        $stream = $null
        Protect-SiemProfileAcl -Path $temporaryPath -Scope $Scope

        $json = $Profile | ConvertTo-Json -Depth 5
        $encoding = New-Object System.Text.UTF8Encoding($true)
        [IO.File]::WriteAllText($temporaryPath, $json, $encoding)

        $targetExists = Test-Path -LiteralPath $Path -PathType Leaf
        if ($targetExists) {
            if (-not $Replace.IsPresent) {
                throw 'SIEM profile was created by another process. Use -Force to replace it.'
            }
            Protect-SiemProfileAcl -Path $Path -Scope $Scope
            Move-SiemProfileFile -TemporaryPath $temporaryPath -Path $Path -Replace
        }
        else {
            Move-SiemProfileFile -TemporaryPath $temporaryPath -Path $Path
        }
        Protect-SiemProfileAcl -Path $Path -Scope $Scope
    }
    finally {
        if ($null -ne $stream) { $stream.Dispose() }
        if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) {
            Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue
        }
    }
}

function Get-SiemObjectProperty {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$Object,

        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    if ($null -eq $Object) { return $null }
    $property = $Object.PSObject.Properties[$Name]
    if ($null -eq $property) { return $null }
    return $property.Value
}

function Assert-SiemNewProfile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Values
    )

    Assert-SiemProfileName -Name $Values.Name
    $destination = $Values.Destination
    $hasSecret = $null -ne $Values.Secret
    if ($hasSecret -and $Values.Secret.Length -eq 0) {
        throw 'Secret cannot be empty.'
    }

    $requireHttps = $destination -in @('SplunkHEC', 'Sentinel', 'SentinelLogsIngestion') -or $Values.ProtectUri
    $null = Assert-SiemEndpointUri -Uri $Values.Uri -Destination $destination -RequireHttps:$requireHttps

    if ($destination -eq 'GenericWebhook') {
        if ($hasSecret) {
            throw 'GenericWebhook profiles do not store arbitrary authorization headers or secrets. Use -ProtectUri for a secret URL.'
        }
        foreach ($field in @('TenantId', 'ClientId', 'MonitorScope', 'WorkspaceId', 'LogType')) {
            if (-not [string]::IsNullOrWhiteSpace([string]$Values[$field])) {
                throw "$field is not valid for GenericWebhook profiles."
            }
        }
        return
    }

    if ($Values.ProtectUri) {
        throw 'ProtectUri is supported only for GenericWebhook profiles.'
    }
    if (-not $hasSecret) {
        throw "$destination profiles require -Secret."
    }

    if ($destination -eq 'SentinelLogsIngestion') {
        if ([string]::IsNullOrWhiteSpace($Values.TenantId) -or [string]::IsNullOrWhiteSpace($Values.ClientId)) {
            throw 'SentinelLogsIngestion profiles require -TenantId and -ClientId.'
        }
        Assert-SiemTenantId -TenantId $Values.TenantId
        Assert-SiemMonitorScope -MonitorScope $Values.MonitorScope
        if (-not [string]::IsNullOrWhiteSpace($Values.WorkspaceId) -or -not [string]::IsNullOrWhiteSpace($Values.LogType)) {
            throw 'WorkspaceId and LogType belong only to the legacy Sentinel destination.'
        }
        return
    }

    foreach ($field in @('TenantId', 'ClientId', 'MonitorScope')) {
        if (-not [string]::IsNullOrWhiteSpace([string]$Values[$field])) {
            throw "$field is valid only for SentinelLogsIngestion profiles."
        }
    }
    if ($destination -eq 'SplunkHEC') {
        if (-not [string]::IsNullOrWhiteSpace($Values.WorkspaceId) -or -not [string]::IsNullOrWhiteSpace($Values.LogType)) {
            throw 'WorkspaceId and LogType are valid only for legacy Sentinel profiles.'
        }
    }
    elseif ($destination -eq 'Sentinel' -and [string]::IsNullOrWhiteSpace($Values.WorkspaceId)) {
        if ($Values.Uri -notmatch '^https://([A-Za-z0-9-]+)\.ods\.opinsights\.azure\.') {
            throw 'Sentinel requires -WorkspaceId or a workspace data collector URI.'
        }
    }
}

function Assert-SiemStoredProfile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$Profile,

        [Parameter(Mandatory = $true)]
        [string]$ExpectedName,

        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$ExpectedScope
    )

    $allowedProperties = @('schemaVersion', 'name', 'type', 'uri', 'scope', 'tenantId', 'clientId', 'monitorScope', 'workspaceId', 'logType', 'secretRef', 'uriRef')
    foreach ($property in @($Profile.PSObject.Properties)) {
        if ($allowedProperties -notcontains $property.Name) {
            throw "Stored profile contains unsupported field '$($property.Name)'."
        }
    }
    if ([string](Get-SiemObjectProperty $Profile 'schemaVersion') -ne '1') {
        throw 'Stored profile has an unsupported or missing schemaVersion.'
    }

    $name = [string](Get-SiemObjectProperty $Profile 'name')
    $type = [string](Get-SiemObjectProperty $Profile 'type')
    $scope = [string](Get-SiemObjectProperty $Profile 'scope')
    Assert-SiemProfileName -Name $name
    if ($name -cne $ExpectedName -or $scope -cne $ExpectedScope) {
        throw 'Stored profile identity does not match its file name or scope.'
    }
    if ($type -notin @('SplunkHEC', 'Sentinel', 'SentinelLogsIngestion', 'GenericWebhook')) {
        throw 'Stored profile has an invalid destination type.'
    }

    $uri = [string](Get-SiemObjectProperty $Profile 'uri')
    $uriRef = [string](Get-SiemObjectProperty $Profile 'uriRef')
    $secretRef = [string](Get-SiemObjectProperty $Profile 'secretRef')
    $tenantId = [string](Get-SiemObjectProperty $Profile 'tenantId')
    $clientId = [string](Get-SiemObjectProperty $Profile 'clientId')
    $monitorScope = [string](Get-SiemObjectProperty $Profile 'monitorScope')
    $workspaceId = [string](Get-SiemObjectProperty $Profile 'workspaceId')
    $logType = [string](Get-SiemObjectProperty $Profile 'logType')
    $expectedPrefix = $(if ($ExpectedScope -eq 'LocalMachine') { 'wsa-dpapi-lm-v1:' } else { 'wsa-dpapi-cu-v1:' })

    if ([string]::IsNullOrWhiteSpace($uriRef)) {
        if ([string]::IsNullOrWhiteSpace($uri)) { throw 'Stored profile has no endpoint URI.' }
        $requireHttps = $type -in @('SplunkHEC', 'Sentinel', 'SentinelLogsIngestion')
        $null = Assert-SiemEndpointUri -Uri $uri -Destination $type -RequireHttps:$requireHttps
    }
    else {
        if ($type -ne 'GenericWebhook' -or -not [string]::IsNullOrWhiteSpace($uri) -or -not $uriRef.StartsWith($expectedPrefix, [StringComparison]::Ordinal)) {
            throw 'Stored profile has an invalid protected URI reference.'
        }
    }

    if ($type -eq 'GenericWebhook') {
        if (-not [string]::IsNullOrWhiteSpace($secretRef)) { throw 'GenericWebhook profiles cannot contain secretRef.' }
        foreach ($value in @($tenantId, $clientId, $monitorScope, $workspaceId, $logType)) {
            if (-not [string]::IsNullOrWhiteSpace($value)) { throw 'GenericWebhook profile contains destination-specific fields.' }
        }
        return
    }

    if ([string]::IsNullOrWhiteSpace($secretRef) -or -not $secretRef.StartsWith($expectedPrefix, [StringComparison]::Ordinal)) {
        throw 'Stored profile is missing a secretRef for the correct DPAPI scope.'
    }
    if (-not [string]::IsNullOrWhiteSpace($uriRef)) { throw 'Only GenericWebhook profiles can contain uriRef.' }

    if ($type -eq 'SentinelLogsIngestion') {
        if ([string]::IsNullOrWhiteSpace($tenantId) -or [string]::IsNullOrWhiteSpace($clientId) -or [string]::IsNullOrWhiteSpace($monitorScope)) {
            throw 'SentinelLogsIngestion profile is missing tenantId, clientId, or monitorScope.'
        }
        Assert-SiemTenantId -TenantId $tenantId
        Assert-SiemMonitorScope -MonitorScope $monitorScope
        if (-not [string]::IsNullOrWhiteSpace($workspaceId) -or -not [string]::IsNullOrWhiteSpace($logType)) {
            throw 'SentinelLogsIngestion profile contains legacy Sentinel fields.'
        }
    }
    else {
        foreach ($value in @($tenantId, $clientId, $monitorScope)) {
            if (-not [string]::IsNullOrWhiteSpace($value)) { throw 'Legacy profile contains OAuth-only fields.' }
        }
        if ($type -eq 'SplunkHEC' -and (-not [string]::IsNullOrWhiteSpace($workspaceId) -or -not [string]::IsNullOrWhiteSpace($logType))) {
            throw 'SplunkHEC profile contains Sentinel-only fields.'
        }
        if ($type -eq 'Sentinel' -and [string]::IsNullOrWhiteSpace($workspaceId) -and $uri -notmatch '^https://([A-Za-z0-9-]+)\.ods\.opinsights\.azure\.') {
            throw 'Sentinel profile is missing workspaceId.'
        }
    }
}

function Read-SiemProfileRecord {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [string]$ExpectedName,

        [Parameter(Mandatory = $true)]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$ExpectedScope
    )

    try {
        $json = [IO.File]::ReadAllText($Path)
        $profile = $json | ConvertFrom-Json -ErrorAction Stop
        if ($profile -is [Array]) { throw 'Stored profile JSON must contain one object.' }
        Assert-SiemStoredProfile -Profile $profile -ExpectedName $ExpectedName -ExpectedScope $ExpectedScope
        return $profile
    }
    catch {
        throw "Could not read SIEM profile '$ExpectedName' ($ExpectedScope): $($_.Exception.Message)"
    }
}

function Resolve-SiemProfile {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter()]
        [ValidateSet('Auto', 'CurrentUser', 'LocalMachine')]
        [string]$Scope = 'Auto'
    )

    Assert-SiemProfileName -Name $Name
    $scopes = $(if ($Scope -eq 'Auto') { @('CurrentUser', 'LocalMachine') } else { @($Scope) })
    $matches = @()
    foreach ($candidateScope in $scopes) {
        $path = Get-SiemProfilePath -Name $Name -Scope $candidateScope
        if (Test-Path -LiteralPath $path -PathType Leaf) {
            $matches += [PSCustomObject]@{
                Name    = $Name
                Scope   = $candidateScope
                Path    = $path
                Profile = (Read-SiemProfileRecord -Path $path -ExpectedName $Name -ExpectedScope $candidateScope)
            }
        }
    }
    if ($matches.Count -eq 0) {
        throw "SIEM profile '$Name' was not found in the requested scope."
    }
    if ($matches.Count -gt 1) {
        throw "SIEM profile '$Name' exists in both CurrentUser and LocalMachine scopes. Specify the scope explicitly."
    }
    return $matches[0]
}

function ConvertTo-SiemProfileMetadata {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$Profile,

        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $uriRef = [string](Get-SiemObjectProperty $Profile 'uriRef')
    $secretRef = [string](Get-SiemObjectProperty $Profile 'secretRef')
    $isUriProtected = -not [string]::IsNullOrWhiteSpace($uriRef)
    return [PSCustomObject]@{
        PSTypeName      = 'WindowsSecurityAudit.SiemProfile'
        SchemaVersion   = [int](Get-SiemObjectProperty $Profile 'schemaVersion')
        Name            = [string](Get-SiemObjectProperty $Profile 'name')
        Destination     = [string](Get-SiemObjectProperty $Profile 'type')
        Scope           = [string](Get-SiemObjectProperty $Profile 'scope')
        Uri             = $(if ($isUriProtected) { $null } else { [string](Get-SiemObjectProperty $Profile 'uri') })
        HasUri          = $true
        IsUriProtected  = $isUriProtected
        HasSecret       = (-not [string]::IsNullOrWhiteSpace($secretRef))
        TenantId        = [string](Get-SiemObjectProperty $Profile 'tenantId')
        ClientId        = [string](Get-SiemObjectProperty $Profile 'clientId')
        MonitorScope    = [string](Get-SiemObjectProperty $Profile 'monitorScope')
        WorkspaceId     = [string](Get-SiemObjectProperty $Profile 'workspaceId')
        LogType         = [string](Get-SiemObjectProperty $Profile 'logType')
        Path            = $Path
    }
}