EntraAuth.Azure.LogAnalytics.psm1

$script:ModuleRoot = $PSScriptRoot

class ServiceTransformAttribute : System.Management.Automation.ArgumentTransformationAttribute {
    [object] Transform([System.Management.Automation.EngineIntrinsics] $Intrinsics, [object] $InputData) {
        if ($null -eq $InputData) {
            return @{ Azure = 'Azure' }
        }
        if ($InputData -is [hashtable]) {
            return $InputData
        }
        if ($InputData -is [ordered]) {
            return $InputData
        }
        if ($InputData -is [string]) {
            return @{ Azure = $InputData }
        }
        if ($InputData.Azure -or $InputData.LogAnalytics) {
            $map = @{ }
            if ($InputData.Azure) { $map.Azure = $InputData.Azure }
            if ($InputData.LogAnalytics) { $map.LogAnalytics = $InputData.LogAnalytics }
            return $map
        }
        return @{ Azure = $InputData -as [string] }
    }
}

[flags()] enum WorkspaceFeatures {
    DataAuthorizationMode = 1
    DisableLocalAuth = 2
    EnableDataExport = 4
    OnlyResourceAccess = 8
    ImmediateDataPurge = 16
}

function ConvertTo-CollectionRule {
    <#
    .SYNOPSIS
        Converts an Azure data collection rule resource to a module collection rule object.
 
    .DESCRIPTION
        Transforms a raw Azure Monitor data collection rule resource into an EntraAuth.Azure.LogAnalytics.DataCollectionRule object with normalized streams, destinations, data flows, status, timestamps, and source-object properties.
 
    .PARAMETER InputObject
        The raw Azure Monitor data collection rule resource to convert. Null input produces no output.
 
    .EXAMPLE
        PS C:\> $ruleResource | ConvertTo-CollectionRule
 
        Converts a raw Azure data collection rule resource into the module's standard collection rule object.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    process {
        if (-not $InputObject) { return }

        $streams = @{}
        foreach ($streamName in $InputObject.properties.streamDeclarations.PSObject.Properties.Name) {
            $streams[$streamName] = [PSCustomObject]@{
                Name    = $streamName
                Columns = $InputObject.properties.streamDeclarations.$streamName.columns
                Object  = $InputObject.properties.streamDeclarations.$streamName
            }
        }

        [PSCustomObject]@{
            PSTypeName         = 'EntraAuth.Azure.LogAnalytics.DataCollectionRule'
            Subscription       = ($InputObject.id -split '/')[2]
            ResourceGroup      = ($InputObject.id -split '/')[4]
            Name               = $InputObject.name
            ID                 = $InputObject.id
            Location           = $InputObject.location

            Streams            = $streams
            DataSources        = $InputObject.properties.dataSources
            Destinations       = $InputObject.properties.destinations
            DataFlows          = $InputObject.properties.dataFlows
            EndpointName       = $InputObject.properties.dataCollectionEndpointId -replace '^.+/'
            EndpointID         = $InputObject.properties.dataCollectionEndpointId

            ImmutableID        = $InputObject.properties.immutableId
            Status             = $InputObject.properties.provisioningState
            Tags               = $InputObject.tags
            Created            = $InputObject.systemData.createdAt
            Modified           = $InputObject.systemData.lastModifiedAt
            Properties         = $InputObject.properties

            CollectionRuleName = $InputObject.name
            
            Object             = $InputObject
        }
    }
}

function ConvertTo-Endpoint {
    <#
    .SYNOPSIS
        Converts an Azure data collection endpoint resource to a module endpoint object.
 
    .DESCRIPTION
        Transforms a raw Azure Monitor data collection endpoint resource into an EntraAuth.Azure.LogAnalytics.DataCollectionEndpoint object with normalized resource, ingestion link, status, timestamp, and source-object properties.
 
    .PARAMETER InputObject
        The raw Azure Monitor data collection endpoint resource to convert. Null input produces no output.
 
    .EXAMPLE
        PS C:\> $endpointResource | ConvertTo-Endpoint
 
        Converts a raw Azure data collection endpoint resource into the module's standard endpoint object.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    process {
        if (-not $InputObject) { return }

        [PSCustomObject]@{
            PSTypeName        = 'EntraAuth.Azure.LogAnalytics.DataCollectionEndpoint'
            Subscription      = ($InputObject.id -split '/')[2]
            ResourceGroup     = ($InputObject.id -split '/')[4]
            Name              = $InputObject.name
            ID                = $InputObject.id
            Location          = $InputObject.location

            LinkIngestion     = $InputObject.properties.logsIngestion.endpoint
            LinkMetrics       = $InputObject.properties.metricsIngestion.endpoint
            LinkConfiguration = $InputObject.properties.configurationAccess.endpoint
            
            ImmutableID       = $InputObject.properties.immutableId
            PublicNetwork     = $InputObject.properties.networkAcls.publicNetworkAccess
            Status            = $InputObject.properties.provisioningState
            Tags              = $InputObject.tags
            Created           = $InputObject.systemData.createdAt
            Modified          = $InputObject.systemData.lastModifiedAt
            Properties        = $InputObject.properties

            EndpointName      = $InputObject.name
            
            Object            = $InputObject
        }
    }
}

function ConvertTo-Table {
    <#
    .SYNOPSIS
        Converts an Azure table resource to a module table object.
 
    .DESCRIPTION
        Transforms a raw Azure Log Analytics table resource into an EntraAuth.Azure.LogAnalytics.Table object with normalized subscription, resource group, workspace, schema, retention, and source-object properties.
 
    .PARAMETER InputObject
        The raw Azure Log Analytics table resource to convert. Null input produces no output.
 
    .EXAMPLE
        PS C:\> $tableResource | ConvertTo-Table
 
        Converts a raw Azure table resource returned by the Azure API into the module's standard table object.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    process {
        if (-not $InputObject) { return }

        [PSCustomObject]@{
            PSTypeName           = 'EntraAuth.Azure.LogAnalytics.Table'
            Subscription         = ($InputObject.id -split '/')[2]
            ResourceGroup        = ($InputObject.id -split '/')[4]
            WorkspaceName        = ($InputObject.id -split '/')[8]
            Name                 = $InputObject.name
            DisplayName          = $InputObject.properties.schema.displayName
            Description          = $InputObject.properties.schema.description
            ID                   = $InputObject.id
            Plan                 = $InputObject.properties.plan
            ProtectionLevel      = $InputObject.properties.protectionLevel
            RetentionInDays      = $InputObject.properties.retentionInDays
            TotalRetentionInDays = $InputObject.properties.totalRetentionInDays
            Solutions            = $InputObject.properties.schema.solutions
            Columns              = $InputObject.properties.schema.columns
            ProvisioningState    = $InputObject.properties.provisioningState
            
            Properties           = $InputObject.properties
            
            TableName            = $InputObject.name
            Object               = $InputObject
        }
    }
}

function ConvertTo-Workspace {
    <#
    .SYNOPSIS
        Converts an Azure workspace resource to a module workspace object.
 
    .DESCRIPTION
        Transforms a raw Azure Log Analytics workspace resource into an EntraAuth.Azure.LogAnalytics.Workspace object with normalized subscription, resource group, status, timestamps, and source-object properties.
 
    .PARAMETER InputObject
        The raw Azure Log Analytics workspace resource to convert. Null input produces no output.
 
    .EXAMPLE
        PS C:\> $workspaceResource | ConvertTo-Workspace
 
        Converts a raw Azure workspace resource returned by the Azure API into the module's standard workspace object.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    process {
        if (-not $InputObject) { return }

        [PSCustomObject]@{
            PSTypeName    = 'EntraAuth.Azure.LogAnalytics.Workspace'
            Subscription  = ($InputObject.id -split '/')[2]
            ResourceGroup = ($InputObject.id -split '/')[4]
            Name          = $InputObject.name
            ID            = $InputObject.id
            Location      = $InputObject.location
            Status        = $InputObject.properties.provisioningState
            Tags          = $InputObject.tags
            Created       = $InputObject.properties.createdDate
            Modified      = $InputObject.properties.modifiedDate
            Properties    = $InputObject.properties

            # For commands taking a workspace by pipeline
            WorkspaceName = $InputObject.name
            
            Object        = $InputObject
        }
    }
}

function Resolve-Subscription {
    <#
    .SYNOPSIS
        Resolves a subscription name or ID to a subscription ID.
 
    .DESCRIPTION
        Returns a supplied subscription ID unchanged or resolves an exact subscription display name through Azure.
        Name resolution succeeds only when exactly one subscription matches and reports an error through the calling cmdlet for missing or ambiguous names.
 
    .PARAMETER Name
        The subscription display name or subscription ID to resolve.
 
    .PARAMETER Services
        A hashtable containing the service mappings used to query subscriptions.
 
    .PARAMETER Cache
        A hashtable caching subscriptions retrieved.
        Use to avoid repeated lookups within a command, but be sure to discard it when switching tenants.
 
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
 
    .EXAMPLE
        PS C:\> Resolve-Subscription -Name 'Production' -Services $services -Cmdlet $PSCmdlet
 
        Resolves the subscription whose display name is Production using the supplied services and returns its subscription ID, reporting any resolution error through the calling cmdlet.
    #>

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

        [Parameter(Mandatory = $true)]
        [hashtable]
        $Services,

        [hashtable]
        $Cache = @{},

        [Parameter(Mandatory = $true)]
        $Cmdlet
    )
    process {
        # We don't validate GUIDs - we assume the user knew better
        # Presumably, bad input would still only lead to subsequent request failing
        if ($Name -as [guid]) { return $Name }

        if ($Cache.Count -lt 1) {
            foreach ($subscription in Get-EaaSubscription -ServiceMap $Services) {
                $Cache[$subscription.id] = $subscription
            }
        }

        $subscriptions = $Cache.Values | Where-Object DisplayName -EQ $Name
        if (@($subscriptions).Count -eq 1) {
            return $subscriptions.SubscriptionID
        }
        if (@($subscriptions).Count -gt 1) {
            Stop-PSFFunction -Message "Ambiguous Subscription! $Name resolved to $(@($subscriptions).Count) subscriptions ($($subscriptions.SubscriptionID -join ', '))" -Cmdlet $Cmdlet -EnableException $true
        }
        Stop-PSFFunction -Message "Invalid Subscription! $Name could not be resolved" -Cmdlet $Cmdlet -EnableException $true
    }
}

function Get-EalaDataCollectionEndpoint {
    <#
    .SYNOPSIS
        Retrieves Azure Monitor data collection endpoints.
 
    .DESCRIPTION
        Retrieves data collection endpoints from an Azure subscription. Results can be scoped to a resource group and filtered by name with wildcard patterns.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the data collection endpoints.
 
    .PARAMETER ResourceGroup
        The resource group containing the data collection endpoints. When omitted, endpoints from all resource groups in the subscription are returned.
 
    .PARAMETER Name
        The endpoint name or wildcard pattern to retrieve. When omitted, all matching endpoints are returned.
        Defaults to: *
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> Get-EalaDataCollectionEndpoint -Subscription 'Production' -ResourceGroup 'rg-monitoring' -Name 'dce-*'
 
        Retrieves all data collection endpoints whose names begin with dce- from the specified resource group.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $Name,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet

        if ($Name -and $Name -notmatch '\*' -and $ResourceGroup) {
            Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.Insights/dataCollectionEndpoints/$Name" -Query @{
                'api-version' = '2024-03-11'
            } | ConvertTo-Endpoint
            return
        }

        if (-not $Name) { $Name = '*' }

        $rgString = ''
        if ($ResourceGroup) { $rgString = "resourceGroups/$ResourceGroup/" }

        Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/$($rgString)providers/Microsoft.Insights/dataCollectionEndpoints" -Query @{
            'api-version' = '2024-03-11'
        } | Where-Object Name -Like $Name | ConvertTo-Endpoint
    }
}

function New-EalaDataCollectionEndpoint {
    <#
    .SYNOPSIS
        Creates an Azure Monitor data collection endpoint.
 
    .DESCRIPTION
        Creates a data collection endpoint with optional network access, platform, identity, tag, and SKU settings. If no location is specified, the resource group's location is used.
 
    .PARAMETER Subscription
        The name or ID of the target Azure subscription.
 
    .PARAMETER ResourceGroup
        The name of the resource group in which to create the endpoint.
 
    .PARAMETER Name
        The name of the data collection endpoint to create.
 
    .PARAMETER Location
        The Azure region in which to create the endpoint.
        Defaults to: Location of the Resource Group
 
    .PARAMETER Description
        A description of the data collection endpoint.
 
    .PARAMETER NetworkAccess
        The public network access mode. Valid values are Enabled, Disabled, and SecuredByPerimeter.
        Defaults to: Enabled
 
    .PARAMETER Tags
        A hashtable of tags to assign to the endpoint.
 
    .PARAMETER Kind
        The operating system kind associated with the endpoint. Valid values are Windows and Linux.
 
    .PARAMETER SystemIdentity
        Enables a system-assigned managed identity on the endpoint.
 
    .PARAMETER Sku
        A hashtable defining the endpoint SKU.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> New-EalaDataCollectionEndpoint -Subscription 'Production' -ResourceGroup 'rg-monitoring' -Name 'dce-prod' -Location 'eastus' -NetworkAccess SecuredByPerimeter -SystemIdentity
 
        Creates a data collection endpoint with perimeter-secured network access and a system-assigned identity.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [string]
        $Location,

        [string]
        $Description,

        [ValidateSet('Enabled', 'Disabled', 'SecuredByPerimeter')]
        [string]
        $NetworkAccess = 'Enabled',

        [hashtable]
        $Tags,

        [ValidateSet('Windows', 'Linux')]
        [string]
        $Kind,

        [switch]
        $SystemIdentity,

        [hashtable]
        $Sku,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}

        # https://learn.microsoft.com/en-us/rest/api/monitor/data-collection-rules/create
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet
        $actualLocation = $Location
        if (-not $actualLocation) {
            $actualLocation = (Get-EaaResourceGroup -ServiceMap $services -Subscription $subscriptionID -Name $ResourceGroup).Location
        }

        $body = @{
            location   = $actualLocation
            properties = @{
                networkAcls = @{
                    publicNetworkAccess = $NetworkAccess
                }
            }
        }
        if ($Description) { $body.properties.description = $Description }
        if ($Kind) {
            $body.kind = $Kind
        }
        if ($Tags) { $body.tags = $Tags }
        if ($Sku) { $body.sku = $Sku }
        if ($SystemIdentity) {
            $body.identity = @{
                tenantId = (Get-EntraToken -Service $services.Azure).TenantId
                type     = 'SystemAssigned'
            }
        }

        $param = @{
            Service     = $services.Azure
            Method      = 'PUT'
            Path        = "subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.Insights/dataCollectionEndpoints/$Name"
            ContentType = 'application/json'
            Query       = @{
                'api-version' = '2024-03-11'
            }
        }

        Invoke-PSFProtectedCommand -Action "Creating Data Collection Endpoint $Name in $subscriptionID > $ResourceGroup" -Target $Name -ScriptBlock {
            Invoke-EntraRequest @param -Body $body | ConvertTo-Endpoint
        } -EnableException $true -PSCmdlet $PSCmdlet
    }
}

function Remove-EalaDataCollectionEndpoint {
    <#
    .SYNOPSIS
        Removes an Azure Monitor data collection endpoint.
 
    .DESCRIPTION
        Deletes a data collection endpoint from an Azure subscription and resource group. Endpoint objects can be supplied through the pipeline by property name.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the endpoint.
 
    .PARAMETER ResourceGroup
        The name of the resource group containing the endpoint.
 
    .PARAMETER Name
        The name of the data collection endpoint to remove.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> Get-EalaDataCollectionEndpoint -Subscription 'Test' -ResourceGroup 'rg-test' -Name 'dce-old' | Remove-EalaDataCollectionEndpoint
 
        Retrieves a data collection endpoint and removes it through the pipeline.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Name,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet

        Invoke-PSFProtectedCommand -Action "Deleting Data Collection Endpoint $Name in $subscriptionID > $ResourceGroup" -Target $Name -ScriptBlock {
            Invoke-EntraRequest -Service $services.Azure -Method DELETE -Path "subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.Insights/dataCollectionEndpoints/$Name" -Query @{
                'api-version' = '2024-03-11'
            }
        } -EnableException $true -PSCmdlet $PSCmdlet
    }
}

function Get-EalaDataCollectionRule {
    <#
    .SYNOPSIS
        Retrieves Azure Monitor data collection rules.
 
    .DESCRIPTION
        Retrieves data collection rules from an Azure subscription.
        Results can be scoped to a resource group and filtered by name with wildcard patterns.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the data collection rules.
 
    .PARAMETER ResourceGroup
        The resource group containing the rules. When omitted, rules from all resource groups in the subscription are returned.
 
    .PARAMETER Name
        The rule name or wildcard pattern to retrieve. When omitted, all matching rules are returned.
        Defaults to: *
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> Get-EalaDataCollectionRule -Subscription 'Production' -ResourceGroup 'rg-monitoring' -Name 'dcr-app-*'
 
        Retrieves data collection rules whose names begin with dcr-app- from the specified resource group.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,
        
        [string]
        $Name,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet

        if ($Name -and $Name -notmatch '\*' -and $ResourceGroup) {
            Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.Insights/dataCollectionRules/$Name" -Query @{
                'api-version' = '2024-03-11'
            } | ConvertTo-CollectionRule
            return
        }

        if (-not $Name) { $Name = '*' }

        $rgString = ''
        if ($ResourceGroup) { $rgString = "resourceGroups/$ResourceGroup/" }

        Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/$($rgString)providers/Microsoft.Insights/dataCollectionRules" -Query @{
            'api-version' = '2024-03-11'
        } | Where-Object Name -Like $Name | ConvertTo-CollectionRule
    }
}

function New-EalaDataCollectionRule {
    <#
    .SYNOPSIS
        Creates an Azure Monitor data collection rule.
 
    .DESCRIPTION
        Creates a data collection rule with configurable data sources, destinations, data flows, references, agent settings, identity, and SKU.
        A Log Analytics workspace can be added automatically as a destination.
 
    .PARAMETER Subscription
        The name or ID of the target Azure subscription.
 
    .PARAMETER ResourceGroup
        The name of the resource group in which to create the rule.
 
    .PARAMETER WorkspaceName
        The name of a Log Analytics workspace to add as a destination.
 
    .PARAMETER TableName
        The name of the table the DCR should send data to.
        Will be ignored if no WorkspaceName is provided.
 
    .PARAMETER Name
        The name of the data collection rule to create.
 
    .PARAMETER Location
        The Azure region in which to create the rule.
        Defaults to: Location of the Resource Group
 
    .PARAMETER Tags
        A hashtable of tags to assign to the rule.
 
    .PARAMETER Kind
        The operating system kind associated with the rule. Valid values are Windows and Linux.
 
    .PARAMETER SystemIdentity
        Enables a system-assigned managed identity on the rule.
 
    .PARAMETER Destinations
        A hashtable defining destinations for collected data.
        Example:
        @{
            logAnalytics = @(
                @{
                    name = 'MyWorkspace'
                    workspaceId = 'bcff068b-18e7-4105-be9c-caba3bea59ce'
                    workspaceResourceId = '16df342b-f30f-4103-986b-12a2feea9992'
                }
            )
        }
        https://learn.microsoft.com/en-us/rest/api/monitor/data-collection-rules/create?view=rest-monitor-2024-03-11&tabs=HTTP#datacollectionruledestinations
 
    .PARAMETER EndpointID
        The Azure resource ID of the data collection endpoint to associate with the rule.
 
    .PARAMETER DataFlows
        An array of hashtables that map data streams to destinations and transformations.
        https://learn.microsoft.com/en-us/rest/api/monitor/data-collection-rules/create?view=rest-monitor-2024-03-11&tabs=HTTP#dataflow
 
    .PARAMETER DataSources
        A hashtable defining the data sources collected by the rule.
        https://learn.microsoft.com/en-us/rest/api/monitor/data-collection-rules/create?view=rest-monitor-2024-03-11&tabs=HTTP#datacollectionruledatasources
 
    .PARAMETER DirectDataSources
        A hashtable defining direct data sources for the rule.
        https://learn.microsoft.com/en-us/rest/api/monitor/data-collection-rules/create?view=rest-monitor-2024-03-11&tabs=HTTP#datacollectionruledirectdatasources
 
    .PARAMETER References
        A hashtable defining reference data used by the rule.
        https://learn.microsoft.com/en-us/rest/api/monitor/data-collection-rules/create?view=rest-monitor-2024-03-11&tabs=HTTP#datacollectionrulereferences
 
    .PARAMETER AgentSettings
        A hashtable defining settings for agents that use the rule.
        https://learn.microsoft.com/en-us/rest/api/monitor/data-collection-rules/create?view=rest-monitor-2024-03-11&tabs=HTTP#datacollectionruleagentsettings
 
    .PARAMETER Sku
        A hashtable defining the rule SKU.
        https://learn.microsoft.com/en-us/rest/api/monitor/data-collection-rules/create?view=rest-monitor-2024-03-11&tabs=HTTP#datacollectionruleresourcesku
 
    .PARAMETER FilePatterns
        What File Patterns to look for on a Monitoring Agent.
        Has no effect on messages/data sent directly, such as through the Write-EalaTableEntry command.
        Defaults to: C:\Test.json
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> New-EalaDataCollectionRule -Subscription 'Production' -ResourceGroup 'rg-monitoring' -WorkspaceName 'law-prod' -Name 'dcr-app' -Location 'eastus' -Kind Windows
 
        Creates a Windows data collection rule and configures the specified workspace as a destination.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [PsfValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{2,61}[A-Za-z0-9]$', ErrorMessage = 'Invalid Name: {0}. Workspace names must be 4-63 characters long and only contain regular letters, numbers, and dashes.')]
        [string]
        $WorkspaceName,

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

        [hashtable]
        $Tags,

        [ValidateSet('Windows', 'Linux')]
        [string]
        $Kind,

        [switch]
        $SystemIdentity,

        [hashtable]
        $Destinations,

        [string]
        $EndpointID,

        [hashtable[]]
        $DataFlows,

        [hashtable]
        $DataSources,

        [hashtable]
        $DirectDataSources,

        [hashtable]
        $References,

        [hashtable]
        $AgentSettings,

        [hashtable]
        $Sku,

        [string[]]
        $FilePatterns = @('C:\test.json'),

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}

        # https://learn.microsoft.com/en-us/rest/api/monitor/data-collection-rules/create
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet

        $actualLocation = $Location
        if (-not $actualLocation) {
            $actualLocation = (Get-EaaResourceGroup -ServiceMap $services -Subscription $subscriptionID -Name $ResourceGroup).Location
        }

        $body = @{
            id         = "/subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.Insights/dataCollectionRules/$Name"
            location   = $actualLocation
            type       = 'Microsoft.Insights/dataCollectionRules'
            name       = $Name
            properties = @{
                dataSources  = @{}
                destinations = @{
                    logAnalytics = @()
                }
            }
        }

        if ($Destinations) {
            foreach ($key in $Destinations.Keys) {
                $body.properties.destinations[$key] = @($Destinations[$key])
            }
        }
        if ($DataSources) {
            foreach ($key in $DataSources.Keys) {
                $body.properties.dataSources[$key] = @($DataSources[$key])
            }
        }
        if ($DirectDataSources) {
            $body.properties.directDataSources = @{}
            foreach ($key in $DirectDataSources.Keys) {
                $body.properties.directDataSources[$key] = @($DirectDataSources[$key])
            }
        }
        if ($References) {
            $body.properties.references = $References
        }
        if ($DataFlows) {
            $body.properties.dataFlows = @($DataFlows)
        }
        if ($EndpointID) {
            $body.properties.dataCollectionEndpointId = $EndpointID
        }
        if ($Kind) {
            $body.kind = $Kind
        }
        if ($AgentSettings) {
            $body.properties.agentSettings = $AgentSettings
        }
        if ($WorkspaceName) {
            $workspaceObject = Get-EalaWorkspace -Subscription $subscriptionID -ResourceGroup $ResourceGroup -Name $WorkspaceName -ServiceMap $services

            $body.properties.destinations.logAnalytics += @{
                workspaceResourceId = $workspaceObject.ID
                workspaceId         = $workspaceObject.Properties.customerId
                name                = $workspaceObject.Name
            }

            if ($TableName) {
                $tableObject = $workspaceObject | Get-EalaTable -Name $TableName

                # 32 characters is the limit
                $streamname = 'Custom-{0}' -f $tableObject.Name
                if ($streamname.Length -gt 32) { $streamname = $streamname.SubString(0, 32) }

                $body.properties.streamDeclarations = @{
                    $streamname = @{
                        columns = @(@($tableObject.Columns).ForEach{ @{ name = $_.name; type = $_.type } })
                    }
                }
                $logFiles = $body.properties.dataSources['logFiles']
                if (-not $logFiles) { $logFiles = @() }
                $logFiles += @{
                    streams      = @($streamname)
                    filePatterns = $FilePatterns
                    format       = 'json'
                    name         = $streamname
                }
                $body.properties.dataSources['logFiles'] = $logFiles

                $dataFlowsTemp = $body.properties.dataFlows
                if (-not $dataFlowsTemp) { $dataFlowsTemp = @() }
                $dataFlowsTemp += @{
                    streams      = @($streamname)
                    destinations = @($workspaceObject.Name)
                    transformKql = 'source'
                    outputStream = $streamname
                }
                $body.properties.dataFlows = $dataFlowsTemp
            }
        }
        if ($Tags) { $body.tags = $Tags }
        if ($Sku) { $body.sku = $Sku }
        if ($SystemIdentity) {
            $body.identity = @{
                tenantId = (Get-EntraToken -Service $services.Azure).TenantId
                type     = 'SystemAssigned'
            }
        }

        # PUT https://management.azure.com/subscriptions/fe923424-d71c-48fc-a446-29f295c1f08c/resourceGroups/rg_psframework/providers/Microsoft.Insights/dataCollectionRules/TestDCR?api-version=2023-03-11&ignoreMissingTables=true
        $param = @{
            Service     = $services.Azure
            Method      = 'PUT'
            Path        = "subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.Insights/dataCollectionRules/$Name"
            ContentType = 'application/json'
            Query       = @{
                'api-version'       = '2024-03-11'
                ignoreMissingTables = $true
            }
        }

        Invoke-PSFProtectedCommand -Action "Creating Data Collection Rule $Name in $subscriptionID > $ResourceGroup" -Target $Name -ScriptBlock {
            Invoke-EntraRequest @param -Body $body | ConvertTo-CollectionRule
        } -EnableException $true -PSCmdlet $PSCmdlet
    }
}

function Remove-EalaDataCollectionRule {
    <#
    .SYNOPSIS
        Removes an Azure Monitor data collection rule.
 
    .DESCRIPTION
        Deletes a data collection rule from an Azure subscription and resource group.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the rule.
 
    .PARAMETER ResourceGroup
        The name of the resource group containing the rule.
 
    .PARAMETER Name
        The name of the data collection rule to remove.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> Get-EalaDataCollectionRule -Subscription 'Test' -ResourceGroup 'rg-test' -Name 'dcr-old' | Remove-EalaDataCollectionRule
 
        Retrieves a data collection rule and removes it through the pipeline.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Name,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet

        Invoke-PSFProtectedCommand -Action "Deleting Data Collection Rule $Name in $subscriptionID > $ResourceGroup" -Target $Name -ScriptBlock {
            Invoke-EntraRequest -Service $services.Azure -Method DELETE -Path "subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.Insights/dataCollectionRules/$Name" -Query @{
                'api-version' = '2024-03-11'
            }
        } -EnableException $true -PSCmdlet $PSCmdlet
    }
}

function Write-EalaTableEntry {
    <#
    .SYNOPSIS
        Writes records to an Azure Monitor Logs table.
 
    .DESCRIPTION
        Sends one or more objects to an Azure Monitor Logs ingestion endpoint through a data collection rule.
        The target stream can be specified directly or inferred from the table name and the streams defined by the rule.
        Endpoint, rule, and authentication data can be cached when the command is called repeatedly.
 
    .PARAMETER Message
        One or more objects to submit as records. The objects are serialized as a JSON array in the ingestion request.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the data collection endpoint and rule.
 
    .PARAMETER ResourceGroup
        The name of the resource group containing the data collection endpoint and rule.
        When omitted, the command searches the subscription for those resources.
 
    .PARAMETER DcrName
        The name of the data collection rule that defines the target stream.
 
    .PARAMETER DceName
        The name of the data collection endpoint used to ingest the records.
 
    .PARAMETER Table
        The table name used to select a matching stream from the data collection rule when Stream is not specified.
        If the rule contains only one stream, that stream is selected automatically.
        Defaults to: <default>
 
    .PARAMETER Stream
        The exact data collection rule stream to which the records are written.
        Use this parameter when the stream cannot be inferred unambiguously from the table name.
 
    .PARAMETER Cache
        A reusable hashtable in which authentication tokens, endpoints, rules, and resolved streams are cached.
        Use the same hashtable across calls to avoid retrieving these values repeatedly.
        Defaults to: @{}
 
    .PARAMETER EntraToken
        An existing EntraAuth access token for https://monitor.azure.com/.
        When omitted, the command obtains a compatible token from the current Azure connection.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> Write-EalaTableEntry -Message $records -Subscription 'Production' -ResourceGroup 'rg-monitoring' -DcrName 'dcr-app' -DceName 'dce-app' -Table 'AppLogs_CL'
 
        Writes the objects in $records to the stream matching the AppLogs_CL table.
 
    .EXAMPLE
        PS C:\> Write-EalaTableEntry -Message $record -Subscription 'Production' -ResourceGroup 'rg-monitoring' -DcrName 'dcr-app' -DceName 'dce-app' -Stream 'Custom-AppLogs' -Cache $cache
 
        Writes a record to an explicitly selected stream and reuses the supplied cache for repeated calls.
    #>

    [CmdletBinding(DefaultParameterSetName = 'ByTable')]
    param (
        [Parameter(Mandatory = $true)]
        [object[]]
        $Message,

        [Parameter(Mandatory = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.LogAnalytics.DataCollectionRule')]
        [string]
        $DcrName,
        
        [Parameter(Mandatory = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.LogAnalytics.DataCollectionEndpoint')]
        [string]
        $DceName,
        
        [Parameter(ParameterSetName = 'ByTable')]
        [string]
        $Table = '<default>',
        
        [Parameter(Mandatory = $true, ParameterSetName = 'ByStream')]
        [string]
        $Stream,

        [hashtable]
        $Cache = @{},

        $EntraToken,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )

    begin {
        if (-not $cache.$Subscription) {
            $cache[$Subscription] = @{
                Endpoint = @{}
                Rule     = @{}
            }
        }
        
        #region Resolve Token
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        if ($EntraToken) { $tokenToUse = $EntraToken }
        elseif ($Cache.$Subscription.Token) { $tokenToUse = $Cache.$Subscription.Token }
        else {
            Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
            $azToken = Get-EntraToken -Service $services.Azure
            $commonParam = @{
                ClientID          = $azToken.ClientID
                TenantID          = $azToken.TenantID
                Resource          = 'https://monitor.azure.com/'
                AuthenticationUrl = $azToken.AuthenticationUrl
            }

            if ($azToken.RefreshToken) {
                $tokenToUse = Connect-EntraService @commonParam -Scopes '.default' -UseRefreshToken
                $Cache.$Subscription.Token = $tokenToUse
            }
            elseif ($azToken.Certificate) {
                $tokenToUse = Connect-EntraService @commonParam -Certificate $azToken.Certificate
                $Cache.$Subscription.Token = $tokenToUse
            }
            elseif ($azToken.ClientSecret) {
                $tokenToUse = Connect-EntraService @commonParam -ClientSecret $azToken.ClientSecret
                $Cache.$Subscription.Token = $tokenToUse
            }
            elseif ($azToken.Type -eq 'Identity') {
                $param = @{}
                if ($azToken.IdentityID) { $param.IdentityID = $azToken.IdentityID }
                if ($azToken.IdentityType) { $param.IdentityType = $azToken.IdentityType }
                $tokenToUse = Connect-EntraService @param -Identity -Resource 'https://monitor.azure.com/' -AuthenticationUrl $azToken.AuthenticationUrl
                $Cache.$Subscription.Token = $tokenToUse
            }
            elseif ($azToken.Type -eq 'Federated') {
                $tokenToUse = Connect-EntraService @commonParam -Federated -FederationProvider $azToken.FederationProvider.Name
                $Cache.$Subscription.Token = $tokenToUse
            }
            elseif ($azToken.Type -eq 'AzAccount') {
                $tokenToUse = Connect-EntraService -AsAzAccount -ShowDialog $azTOken.ShowDialog -Resource 'https://monitor.azure.com/' -AuthenticationUrl $azToken.AuthenticationUrl
                $Cache.$Subscription.Token = $tokenToUse
            }
        }
        #endregion Resolve Token
    }
    process {
        $commonParam = @{
            Subscription = $Subscription
            ServiceMap   = $ServiceMap
        }
        if ($ResourceGroup) { $commonParam.ResourceGroup = $ResourceGroup }

        if (-not $Cache.$Subscription.Endpoint[$DceName]) {
            $Cache.$Subscription.Endpoint[$DceName] = Get-EalaDataCollectionEndpoint @commonParam -Name $DceName
            if (-not $Cache.$Subscription.Endpoint[$DceName]) { Stop-PSFFunction -String 'Write-EalaTableEntry.Error.EndpointNotFound' -StringValues $DceName -EnableException $true -Category ObjectNotFound -Cmdlet $PSCmdlet }
        }
        if (-not $Cache.$Subscription.Rule[$DcrName]) {
            $Cache.$Subscription.Rule[$DcrName] = Get-EalaDataCollectionRule @commonParam -Name $DcrName | Add-Member -MemberType NoteProperty -Name _Streams -Value @{} -PassThru -Force
            if (-not $Cache.$Subscription.Rule[$DcrName]) { Stop-PSFFunction -String 'Write-EalaTableEntry.Error.RuleNotFound' -StringValues $DcrName -EnableException $true -Category ObjectNotFound -Cmdlet $PSCmdlet }
        }
        
        #region Calculate Stream
        if ($Stream) { $streamName = $Stream }
        else {
            $streams = $Cache.$Subscription.Rule[$DcrName].properties.dataFlows.streams
            if ($Cache.$Subscription.Rule[$DcrName]._Streams.$Table) {
                $streamName = $Cache.$Subscription.Rule[$DcrName]._Streams.$Table
            }
            elseif (@($streams).Count -eq 1) {
                $streamName = $($streams)
                $Cache.$Subscription.Rule[$DcrName]._Streams[$Table] = $streamName
            }
            elseif (@($streams | Where-Object { $_ -match $Table }).Count -eq 1) {
                $streamName = $streams | Where-Object { $_ -match $Table }
                $Cache.$Subscription.Rule[$DcrName]._Streams[$Table] = $streamName
            }
            elseif (-not $streams) {
                Stop-PSFFunction -String 'Write-EalaTableEntry.Error.NoStreams' -StringValues $DcrName -EnableException $true -Category InvalidData -Cmdlet $PSCmdlet
            }
            else {
                Stop-PSFFunction -String 'Write-EalaTableEntry.Error.AmbiguousStreams' -StringValues ($streams -join ', '), $Table -EnableException $true -Category InvalidData -Cmdlet $PSCmdlet
            }
        }
        #endregion Calculate Stream
    
        Invoke-EntraRequest -Method Post -Path "$($Cache.$Subscription.Endpoint[$DceName].LinkIngestion)/dataCollectionRules/$($Cache.$Subscription.Rule[$DcrName].ImmutableId)/streams/$streamName" -Query @{
            'api-version' = '2023-01-01'
        } -ContentType 'application/json' -Body @($Message) -Token $tokenToUse
    }
}

function Get-EalaColumnTemplate {
    <#
    .SYNOPSIS
        Retrieves registered table column templates.
 
    .DESCRIPTION
        Returns column templates registered in the current module session. Template names can be filtered with a wildcard pattern.
 
    .PARAMETER Name
        The template name or wildcard pattern to retrieve.
        Defaults to: *
 
    .EXAMPLE
        PS C:\> Get-EalaColumnTemplate -Name 'Audit*'
 
        Returns all registered column templates whose names begin with Audit.
    #>

    [CmdletBinding()]
    param (
        [PsfArgumentCompleter('EntraAuth.Azure.LogAnalytics.TableColumnTemplate')]
        [string]
        $Name = '*'
    )
    process {
        $script:_TableColumnTemplates.Values | Where-Object Name -Like $Name
    }
}

function Get-EalaTable {
    <#
    .SYNOPSIS
        Retrieves tables from a Log Analytics workspace.
 
    .DESCRIPTION
        Retrieves Log Analytics tables and filters them by name.
        Unless Literal is specified, the filter also matches the custom-table _CL suffix.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the workspace.
 
    .PARAMETER ResourceGroup
        The name of the resource group containing the workspace.
 
    .PARAMETER WorkspaceName
        The name of the Log Analytics workspace containing the tables.
 
    .PARAMETER Name
        The table name or wildcard pattern to retrieve.
        Defaults to: *
 
    .PARAMETER Literal
        Matches only the supplied table name pattern and disables automatic matching of the _CL custom-table suffix.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> Get-EalaTable -Subscription 'Production' -ResourceGroup 'rg-monitoring' -WorkspaceName 'law-prod' -Name 'AppLogs'
 
        Retrieves tables matching AppLogs or AppLogs_CL from the specified workspace.
 
    .EXAMPLE
        PS C:\> Get-EalaWorkspace -Subscription 570b5874-4ced-4cc7-92ad-82308ccc7f93 -Name pslogging | Get-EalaTable -Name ps_*
 
        Retrieves all tables that start with "ps_" in the workspace "pslogging" under the specified subscription.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{2,61}[A-Za-z0-9]$', ErrorMessage = 'Invalid Name: {0}. Workspace names must be 4-63 characters long and only contain regular letters, numbers, and dashes.')]
        [string]
        $WorkspaceName,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $Name = '*',

        [switch]
        $Literal,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet

        Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.OperationalInsights/workspaces/$WorkspaceName/tables/" -Query @{
            'api-version' = '2026-03-01'
        } | ConvertTo-Table | Where-Object {
            $_.Name -like $Name -or
            (
                -not $Literal -and
                $_.Name -like "$($Name)_CL"
            )
        }
    }
}

function New-EalaTable {
    <#
    .SYNOPSIS
        Creates a table in a Log Analytics workspace.
 
    .DESCRIPTION
        Creates a Log Analytics table from an explicit column schema or a registered column template.
        Custom table names receive the _CL suffix unless Literal is specified.
 
    .PARAMETER Subscription
        The name or ID of the target Azure subscription.
 
    .PARAMETER ResourceGroup
        The name of the resource group containing the workspace.
 
    .PARAMETER WorkspaceName
        The name of the Log Analytics workspace in which to create the table.
 
    .PARAMETER Name
        The table name. Unless Literal is specified, _CL is appended when it is not already present.
 
    .PARAMETER Plan
        The table plan. Options:
         
        - Basic: Can be queried, may have significant delays, lower cost
        - Analytics: All features, suitable for realtimne detection, but high cost
        - Auxiliary: Long Term Archive that is rarely read. Lowest cost.
 
        Defaults to: Basic
 
    .PARAMETER Categories
        The categories to associate with the table.
        Defaults to: @()
 
    .PARAMETER Columns
        An array of hashtables defining the table columns.
        Defaults to: @()
 
        Example:
        @{
            name = 'Message'
            type = 'string'
        }
        @{
            name = 'TimeGenerated'
            type = 'datetime'
        }
        @{
            name = 'Level'
            type = 'string'
        }
        @{
            name = 'Tags'
            type = 'dynamic'
        }
 
        Types: string, int, long, real, boolean, dateTime, guid, dynamic
 
        https://learn.microsoft.com/en-us/rest/api/loganalytics/tables/create-or-update?view=rest-loganalytics-2026-03-01&tabs=HTTP#column
 
    .PARAMETER ColumnTemplate
        The name of a registered column template.
        Column templates / presets can be defined using "Register-EalaColumnTemplate" and searched using "Get-EalaColumnTemplate".
 
    .PARAMETER Description
        A description of the table.
 
    .PARAMETER DisplayName
        The display name of the table.
 
    .PARAMETER Labels
        The labels to associate with the table.
 
    .PARAMETER Solutions
        The Azure solutions associated with the table.
 
    .PARAMETER RetentionInDays
        The interactive retention period, from 4 through 730 days. This parameter is supported only for the Analytics plan.
 
    .PARAMETER TotalRetentionInDays
        The total retention period, from 4 through 4383 days.
 
    .PARAMETER ProtectionLevel
        The table data protection level. Valid values are General and Protected.
 
    .PARAMETER Literal
        Uses Name exactly as supplied and disables automatic addition of the _CL suffix.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> New-EalaTable -Subscription 'Production' -ResourceGroup 'rg-monitoring' -WorkspaceName 'law-prod' -Name 'AppMetrics' -Plan Analytics -RetentionInDays 90 -ColumnTemplate 'StandardMetrics'
 
        Creates AppMetrics_CL from a registered column template with 90 days of interactive retention.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{2,61}[A-Za-z0-9]$', ErrorMessage = 'Invalid Name: {0}. Workspace names must be 4-63 characters long and only contain regular letters, numbers, and dashes.')]
        [string]
        $WorkspaceName,

        [Parameter(Mandatory = $true)]
        [PsfValidatePattern('^[A-Za-z0-9-_]+$', ErrorMessage = 'Invalid Name: {0}. Table-names may only contain default letters of the 26-letter alphabet, numbers, dash and underscore')]
        [string]
        $Name,

        [ValidateSet('Basic', 'Analytics', 'Auxiliary')]
        [string]
        $Plan = 'Basic',

        [string[]]
        $Categories = @(),

        [Parameter(Mandatory = $true, ParameterSetName = 'Columns')]
        [PsfValidateScript('EntraAuth.Azure.LogAnalytics.TableColumnValidation')]
        [hashtable[]]
        $Columns,
        
        [Parameter(Mandatory = $true, ParameterSetName = 'Template')]
        [PsfArgumentCompleter('EntraAuth.Azure.LogAnalytics.TableColumnTemplate')]
        [PsfValidateSet(TabCompletion = 'EntraAuth.Azure.LogAnalytics.TableColumnTemplate')]
        [string]
        $ColumnTemplate,

        [string]
        $Description,

        [string]
        $DisplayName,

        [string[]]
        $Labels,

        [string[]]
        $Solutions,

        [ValidateRange(4, 730)]
        [int]
        $RetentionInDays = -1,

        [ValidateRange(4, 4383)]
        [int]
        $TotalRetentionInDays = -1,

        [ValidateSet('General', 'Protected')]
        [string]
        $ProtectionLevel,

        [switch]
        $Literal,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}

        if ($PSBoundParameters.Keys -contains 'RetentionInDays' -and $Plan -ne 'Analytics') {
            Stop-PSFFunction -Message '"RetentionInDays" can be only specified in combination with an "Analytics" Plan' -EnableException $true -Cmdlet $PSCmdlet
        }

        if ($Columns) { $effectiveColumns = $Columns }
        else { $effectiveColumns = $script:_TableColumnTemplates[$ColumnTemplate].Columns }

        if (-not $effectiveColumns) {
            Stop-PSFFunction -Message 'No columns specified / resolved to!' -EnableException $true -Cmdlet $PSCmdlet
        }

        if (-not $Literal) {
            if ($Name -notmatch '_CL$') {
                $Name = "$($Name)_CL"
            }
        }
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet

        $body = @{
            properties = @{
                plan                 = $Plan
                schema               = @{
                    columns = @($effectiveColumns)
                    name    = $Name
                }
                totalRetentionInDays = $TotalRetentionInDays
            }
        }
        if ($ProtectionLevel) { $body.properties.protectionLevel = $ProtectionLevel }
        if ($Plan -eq 'Analytics') { $body.properties.retentionInDays = $RetentionInDays }
        if ($Categories) { $body.properties.schema.categories = @($Categories) }
        if ($Description) { $body.properties.schema.description = $Description }
        if ($DisplayName) { $body.properties.schema.displayName = $DisplayName }
        if ($Labels) { $body.properties.schema.labels = @($Labels) }
        if ($Solutions) { $body.properties.schema.solutions = @($Solutions) }

        Invoke-PSFProtectedCommand -Action "Creating table $Name in $subscriptionID > $ResourceGroup > $WorkspaceName" -Target $Name -ScriptBlock {
            Invoke-EntraRequest -Service $services.Azure -Method PUT -Path "subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.OperationalInsights/workspaces/$WorkspaceName/tables/$Name" -Query @{
                'api-version' = '2026-03-01'
            } -Body $body -ContentType 'application/json' | ConvertTo-Table
        } -EnableException $true -PSCmdlet $PSCmdlet
    }
}

function Register-EalaColumnTemplate {
    <#
    .SYNOPSIS
        Registers a reusable table column template.
 
    .DESCRIPTION
        Registers a named column schema in the current module session for use by New-EalaTable.
        This allows a module that needs a Log Analytics table to prepare just how that should be set up, simplifying deployment for users of that module.
 
    .PARAMETER Name
        The unique name used to identify the column template.
 
    .PARAMETER Description
        A description of the template and its intended use.
 
    .PARAMETER Columns
        An array of hashtables defining the names and data types of the template columns.
 
        Example:
        @{
            name = 'Message'
            type = 'string'
        }
        @{
            name = 'TimeGenerated'
            type = 'datetime'
        }
        @{
            name = 'Level'
            type = 'string'
        }
        @{
            name = 'Tags'
            type = 'dynamic'
        }
 
        Types: string, int, long, real, boolean, dateTime, guid, dynamic
 
        Documentation:
        https://learn.microsoft.com/en-us/rest/api/loganalytics/tables/create-or-update?view=rest-loganalytics-2026-03-01&tabs=HTTP#column
 
    .EXAMPLE
        PS C:\> Register-EalaColumnTemplate -Name 'AuditLog' -Description 'Standard audit event columns' -Columns @(@{ Name = 'EventId'; Type = 'int' }, @{ Name = 'Message'; Type = 'string' })
 
        Registers an AuditLog template containing integer EventId and string Message columns.
    #>

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

        [Parameter(Mandatory = $true)]
        [PsfValidateScript('EntraAuth.Azure.LogAnalytics.TableColumnValidation')]
        [hashtable[]]
        $Columns
    )
    process {
        $script:_TableColumnTemplates[$Name] = [PSCustomObject]@{
            PSTypeName  = 'EntraAuth.Azure.LogAnalytics.ColumnTemplate'
            Name        = $Name
            Description = $Description
            Columns     = $Columns
        }
    }
}

function Remove-EalaTable {
    <#
    .SYNOPSIS
        Removes a table from a Log Analytics workspace.
 
    .DESCRIPTION
        Deletes a table from a Log Analytics workspace.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the workspace.
 
    .PARAMETER ResourceGroup
        The name of the resource group containing the workspace.
 
    .PARAMETER WorkspaceName
        The name of the Log Analytics workspace containing the table.
 
    .PARAMETER Name
        The name of the table to remove.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> Get-EalaTable -Subscription 'Test' -ResourceGroup 'rg-test' -WorkspaceName 'law-test' -Name 'TempData' | Remove-EalaTable
 
        Retrieves a table and removes it through the pipeline.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{2,61}[A-Za-z0-9]$', ErrorMessage = 'Invalid Name: {0}. Workspace names must be 4-63 characters long and only contain regular letters, numbers, and dashes.')]
        [string]
        $WorkspaceName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfValidatePattern('^[A-Za-z0-9-_]+$', ErrorMessage = 'Invalid Name: {0}. Table-names may only contain default letters of the 26-letter alphabet, numbers, dash and underscore')]
        [string]
        $Name,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet

        Invoke-PSFProtectedCommand -Action "Deleting table $Name from $subscriptionID > $ResourceGroup > $WorkspaceName" -Target $Name -ScriptBlock {
            Invoke-EntraRequest -Service $services.Azure -Method DELETE -Path "subscriptions/$subscriptionID/resourceGroups/$ResourceGroup/providers/Microsoft.OperationalInsights/workspaces/$WorkspaceName/tables/$Name" -Query @{
                'api-version' = '2026-03-01'
            }
        } -EnableException $true -PSCmdlet $PSCmdlet
    }
}

function Get-EalaWorkspace {
    <#
    .SYNOPSIS
        Retrieves Log Analytics workspaces.
 
    .DESCRIPTION
        Retrieves Log Analytics workspaces from an Azure subscription.
        Results can be scoped to a resource group and filtered by workspace name.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the workspaces.
 
    .PARAMETER ResourceGroup
        The resource group containing the workspaces. When omitted, workspaces from all resource groups in the subscription are returned.
 
    .PARAMETER Name
        The name of the workspace to retrieve. When omitted, all matching workspaces are returned.
        Defaults to: *
 
    .PARAMETER ID
        The full Azure Resource ID of the Workspace to retrieve.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> Get-EalaWorkspace -Subscription 'Production' -ResourceGroup 'rg-monitoring' -Name 'law-prod'
 
        Retrieves the law-prod workspace from the specified resource group.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Search')]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,
        
        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Search')]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,
        
        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Search')]
        [PsfValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{2,61}[A-Za-z0-9]$', ErrorMessage = 'Invalid Name: {0}. Workspace names must be 4-63 characters long and only contain regular letters, numbers, and dashes.')]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByID')]
        $ID,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}
    }
    process {
        if ($ID) {
            Invoke-EntraRequest -Service $services.Azure -Path $ID -Query @{
                'api-version' = '2026-03-01'
            } | ConvertTo-Workspace
            return
        }

        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet
        $rgString = ''
        if ($ResourceGroup) { $rgString = "resourcegroups/$ResourceGroup/" }
        
        if ($Name -and $ResourceGroup) {
            Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/$($rgString)providers/Microsoft.OperationalInsights/workspaces/$Name" -Query @{
                'api-version' = '2026-03-01'
            } | ConvertTo-Workspace
            return
        }
        $nameFilter = '*'
        if ($Name) { $nameFilter = $Name }
        
        Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/$($rgString)providers/Microsoft.OperationalInsights/workspaces" -Query @{
            'api-version' = '2026-03-01'
        } | ConvertTo-Workspace | Where-Object Name -Like $nameFilter
    }
}

function New-EalaWorkspace {
    <#
    .SYNOPSIS
        Creates a Log Analytics workspace.
 
    .DESCRIPTION
        Creates a Log Analytics workspace with optional tags, identity, retention, SKU, feature flags, replication, and custom properties.
 
    .PARAMETER Subscription
        The name or ID of the target Azure subscription.
 
    .PARAMETER ResourceGroup
        The name of the resource group in which to create the workspace.
 
    .PARAMETER Name
        The name of the Log Analytics workspace to create.
 
    .PARAMETER Location
        The Azure region in which to create the workspace.
        Defaults to: Location of the Resource Group
 
    .PARAMETER Etag
        An entity tag used for optimistic concurrency control.
 
    .PARAMETER Tags
        A hashtable of tags to assign to the workspace.
 
    .PARAMETER SystemIdentity
        Enables a system-assigned managed identity on the workspace.
 
    .PARAMETER RetentionDays
        The workspace data retention period in days.
        The minimum value is 4.
 
    .PARAMETER Sku
        The pricing SKU to assign to the workspace.
 
    .PARAMETER Features
        One or more WorkspaceFeatures flags to enable.
 
    .PARAMETER ReplicationLocation
        The secondary Azure region in which to enable workspace replication.
 
    .PARAMETER Properties
        A hashtable of additional workspace properties. These values override properties generated by the command.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> New-EalaWorkspace -Subscription 'Production' -ResourceGroup 'rg-monitoring' -Name 'law-prod' -Location 'eastus' -Sku 'PerGB2018' -RetentionDays 90 -Features DisableLocalAuth
 
        Creates a workspace with 90-day retention and local authentication disabled.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(Mandatory = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true)]
        [PsfValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{2,61}[A-Za-z0-9]$', ErrorMessage = 'Invalid Name: {0}. Workspace names must be 4-63 characters long and only contain regular letters, numbers, and dashes.')]
        [string]
        $Name,

        [PsfArgumentCompleter('EntraAuth.Azure.Location')]
        [string]
        $Location,

        [string]
        $Etag,

        [hashtable]
        $Tags,

        [switch]
        $SystemIdentity,

        [Alias('retentionInDays')]
        [int]
        $RetentionDays,

        [PsfArgumentCompleter('EntraAuth.Azure.LogAnalytics.Sku')]
        [string]
        $Sku,

        [WorkspaceFeatures]
        $Features,

        [string]
        $ReplicationLocation,

        [hashtable]
        $Properties,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cmdlet $PSCmdlet
    }
    process {
        $actualLocation = $Location
        if (-not $actualLocation) {
            $actualLocation = (Get-EaaResourceGroup -ServiceMap $services -Subscription $subscriptionID -Name $ResourceGroup).Location
        }

        $body = @{
            location = $actualLocation
        }
        if ($Etag) { $body.etag = $Etag }
        if ($SystemIdentity) {
            $body.identity = @{
                tenantId = (Get-EntraToken -Service $services.Azure).TenantId
                type     = 'SystemAssigned'
            }
        }
        if ($Tags) { $body.tags = $Tags }

        $propertySet = @{}
        foreach ($key in $Properties.Keys) {
            $propertySet[$key] = $Properties[$key]
        }
        if ($PSBoundParameters.Keys -contains 'RetentionDays') { $propertySet['retentionInDays'] = $RetentionDays }
        if ($PSBoundParameters.Keys -contains 'Sku') { $propertySet['sku'] = @{ name = $Sku } }
        if ($PSBoundParameters.Keys -contains 'Features') {
            $propertySet['features'] = @{ }
            if ($Features -band [WorkspaceFeatures]::DataAuthorizationMode) { $propertySet['features']['dataAuthorizationMode'] = $true }
            if ($Features -band [WorkspaceFeatures]::DisableLocalAuth) { $propertySet['features']['disableLocalAuth'] = $true }
            if ($Features -band [WorkspaceFeatures]::EnableDataExport) { $propertySet['features']['enableDataExport'] = $true }
            if ($Features -band [WorkspaceFeatures]::OnlyResourceAccess) { $propertySet['features']['enableLogAccessUsingOnlyResourcePermissions'] = $true }
            if ($Features -band [WorkspaceFeatures]::ImmediateDataPurge) { $propertySet['features']['immediatePurgeDataOn30Days'] = $true }
        }
        if ($ReplicationLocation) {
            $propertySet.replication = @{
                enabled  = $true
                location = $ReplicationLocation
            }
        }
        $body.properties = $propertySet

        Invoke-PSFProtectedCommand -Action "Creating Azure Log Analytics Workspace $Name in $SubscriptionID/$ResourceGroup" -Target $Name -ScriptBlock {
            Invoke-EntraRequest -Service $services.Azure -Method PUT -Path "subscriptions/$subscriptionID/resourcegroups/$ResourceGroup/providers/Microsoft.OperationalInsights/workspaces/$Name" -Query @{
                'api-version' = '2026-03-01'
            } -Body $body -ContentType 'application/json' | ConvertTo-Workspace
        } -EnableException $true -PSCmdlet $PSCmdlet
    }
}

function Remove-EalaWorkspace {
    <#
    .SYNOPSIS
        Removes a Log Analytics workspace.
 
    .DESCRIPTION
        Deletes a Log Analytics workspace from an Azure subscription and resource group.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the workspace.
 
    .PARAMETER ResourceGroup
        The name of the resource group containing the workspace.
 
    .PARAMETER Name
        The name of the Log Analytics workspace to remove.
 
    .PARAMETER Force
        Request forced deletion.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> Remove-EalaWorkspace -Subscription 'Test' -ResourceGroup 'rg-test' -Name 'law-old' -Force
 
        Forces deletion of the specified Log Analytics workspace.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{2,61}[A-Za-z0-9]$', ErrorMessage = 'Invalid Name: {0}. Workspace names must be 4-63 characters long and only contain regular letters, numbers, and dashes.')]
        [string]
        $Name,

        [switch]
        $Force,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet
        $query = @{
            'api-version' = '2026-03-01'
        }
        if ($Force) { $query.force = $true }

        Invoke-PSFProtectedCommand -Action "Deleting Azure Log Analytics Workspace $Name in $SubscriptionID/$ResourceGroup" -Target $Name -ScriptBlock {
            $null = Invoke-EntraRequest -Service $services.Azure -Method Delete -Path "subscriptions/$subscriptionID/resourcegroups/$ResourceGroup/providers/Microsoft.OperationalInsights/workspaces/$Name" -Query $query
        } -PSCmdlet $PSCmdlet -EnableException $true
    }
}

function Set-EalaWorkspace {
    <#
    .SYNOPSIS
        Updates a Log Analytics workspace.
 
    .DESCRIPTION
        Updates tags, identity, retention, SKU, features, replication, or custom properties on an existing Log Analytics workspace. At least one setting must be specified.
 
    .PARAMETER Subscription
        The name or ID of the Azure subscription containing the workspace.
 
    .PARAMETER ResourceGroup
        The name of the resource group containing the workspace.
 
    .PARAMETER Name
        The name of the Log Analytics workspace to update.
 
    .PARAMETER Tags
        A hashtable of tags to apply to the workspace.
 
    .PARAMETER SystemIdentity
        Enables a system-assigned managed identity on the workspace.
 
    .PARAMETER RetentionDays
        The workspace data retention period in days.
 
    .PARAMETER Sku
        The pricing SKU to assign to the workspace.
 
    .PARAMETER EnableFeatures
        One or more WorkspaceFeatures flags to enable.
 
    .PARAMETER DisableFeatures
        One or more WorkspaceFeatures flags to disable.
 
    .PARAMETER ReplicationLocation
        The secondary Azure region in which to enable workspace replication.
 
    .PARAMETER DisableReplication
        Disables replication for the workspace.
 
    .PARAMETER Properties
        A hashtable of additional workspace properties to update.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
 
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .PARAMETER ServiceMap
        Optional hashtable to map service names to specific EntraAuth service instances.
        Used for advanced scenarios where you want to use something other than the default Azure connection.
        Example: @{ Azure = 'MyAzure' }
        This will switch all Azure API calls to use the configuration defined in MyAzure.
        Defaults to: @{}
 
    .EXAMPLE
        PS C:\> Set-EalaWorkspace -Subscription 'Production' -ResourceGroup 'rg-monitoring' -Name 'law-prod' -RetentionDays 180 -EnableFeatures EnableDataExport -ReplicationLocation 'westus'
 
        Sets retention to 180 days, enables data export, and configures replication to westus.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.Subscription')]
        [string]
        $Subscription,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup')]
        [string]
        $ResourceGroup,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{2,61}[A-Za-z0-9]$', ErrorMessage = 'Invalid Name: {0}. Workspace names must be 4-63 characters long and only contain regular letters, numbers, and dashes.')]
        [string]
        $Name,

        [hashtable]
        $Tags,

        [switch]
        $SystemIdentity,

        [Alias('retentionInDays')]
        [int]
        $RetentionDays,

        [PsfArgumentCompleter('EntraAuth.Azure.LogAnalytics.Sku')]
        [string]
        $Sku,

        [Alias('Features')]
        [WorkspaceFeatures]
        $EnableFeatures,

        [WorkspaceFeatures]
        $DisableFeatures,

        [string]
        $ReplicationLocation,

        [switch]
        $DisableReplication,

        [hashtable]
        $Properties,

        [ServiceTransformAttribute()]
        [hashtable]
        $ServiceMap = @{}
    )
    begin {
        $services = $script:_serviceSelector.GetServiceMap($ServiceMap)
        Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure
        $subCache = @{}
    }
    process {
        $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cache $subCache -Cmdlet $PSCmdlet

        $body = @{}

        if ($SystemIdentity) {
            $body.identity = @{
                tenantId = (Get-EntraToken -Service $services.Azure).TenantId
                type     = 'SystemAssigned'
            }
        }
        if ($Tags) { $body.tags = $Tags }

        $propertySet = @{}
        foreach ($key in $Properties.Keys) {
            $propertySet[$key] = $Properties[$key]
        }
        if ($PSBoundParameters.Keys -contains 'RetentionDays') { $propertySet['retentionInDays'] = $RetentionDays }
        if ($PSBoundParameters.Keys -contains 'Sku') { $propertySet['sku'] = @{ name = $Sku } }
        
        $features = @{ }
        if ($PSBoundParameters.Keys -contains 'EnableFeatures') {
            if ($EnableFeatures -band [WorkspaceFeatures]::DataAuthorizationMode) { $features['dataAuthorizationMode'] = $true }
            if ($EnableFeatures -band [WorkspaceFeatures]::DisableLocalAuth) { $features['disableLocalAuth'] = $true }
            if ($EnableFeatures -band [WorkspaceFeatures]::EnableDataExport) { $features['enableDataExport'] = $true }
            if ($EnableFeatures -band [WorkspaceFeatures]::OnlyResourceAccess) { $features['enableLogAccessUsingOnlyResourcePermissions'] = $true }
            if ($EnableFeatures -band [WorkspaceFeatures]::ImmediateDataPurge) { $features['immediatePurgeDataOn30Days'] = $true }
        }
        if ($PSBoundParameters.Keys -contains 'DisableFeatures') {
            if ($DisableFeatures -band [WorkspaceFeatures]::DataAuthorizationMode) { $features['dataAuthorizationMode'] = $false }
            if ($DisableFeatures -band [WorkspaceFeatures]::DisableLocalAuth) { $features['disableLocalAuth'] = $false }
            if ($DisableFeatures -band [WorkspaceFeatures]::EnableDataExport) { $features['enableDataExport'] = $false }
            if ($DisableFeatures -band [WorkspaceFeatures]::OnlyResourceAccess) { $features['enableLogAccessUsingOnlyResourcePermissions'] = $false }
            if ($DisableFeatures -band [WorkspaceFeatures]::ImmediateDataPurge) { $features['immediatePurgeDataOn30Days'] = $false }
        }
        if ($features.Count -gt 0) { $propertySet['features'] = $features }

        if ($ReplicationLocation) {
            $propertySet.replication = @{
                enabled  = $true
                location = $ReplicationLocation
            }
        }
        if ($DisableReplication) {
            $propertySet.replication = @{
                enabled = $false
            }
        }
        if ($propertySet.Count -gt 0) { $body.properties = $propertySet }

        if ($body.Count -lt 1) {
            Write-Error 'No changes specified, nothing to do!'
            return
        }

        Invoke-PSFProtectedCommand -Action "Creating Azure Log Analytics Workspace $Name in $SubscriptionID/$ResourceGroup" -Target $Name -ScriptBlock {
            Invoke-EntraRequest -Service $services.Azure -Method PATCH -Path "subscriptions/$subscriptionID/resourcegroups/$ResourceGroup/providers/Microsoft.OperationalInsights/workspaces/$Name" -Query @{
                'api-version' = '2026-03-01'
            } -Body $body -ContentType 'application/json' | ConvertTo-Workspace
        } -EnableException $true -PSCmdlet $PSCmdlet
    }
}

# Module-wide variables go here
# For example if you want to cache some data, have some module-wide config settings, etc. ... those could go here
# Example:
# $script:config = @{ }
$script:_services = @{
    Azure        = 'Azure'
    LogAnalytics = 'LogAnalytics'
}

$script:_serviceSelector = New-EntraServiceSelector -DefaultServices $script:_services

# Table Templates for easier integration into other modules that need Log Analytics Workspace Tables
$script:_TableColumnTemplates = @{}

# Commands run on module import go here
# E.g. Argument Completers could be placed here

Import-PSFLocalizedString -Path "$script:ModuleRoot/en-us/strings.psd1" -Module EntraAuth.Azure.LogAnalytics -Language en-US

Register-PSFTeppScriptblock -Name 'EntraAuth.Azure.LogAnalytics.Sku' -ScriptBlock {
    'Free', 'Standard', 'Premium', 'PerNode', 'PerGB2018', 'Standalone', 'CapacityReservation', 'LACluster'
}

Register-PSFTeppScriptblock -Name 'EntraAuth.Azure.LogAnalytics.TableColumnTemplate' -ScriptBlock {
    Get-EalaColumnTemplate | ForEach-Object {
        @{
            Text    = $_.Name
            Tooltip = $_.Description
        }
    }
}

Register-PSFTeppScriptblock -Name 'EntraAuth.Azure.LogAnalytics.DataCollectionEndpoint' -ScriptBlock {
    if (-not $fakeBoundParameter.Subscription) { return }
    $param = @{ Subscription = $fakeBoundParameter.Subscription }
    if ($fakeBoundParameter.ResourceGroup) { $param.ResourceGroup = $fakeBoundParameter.ResourceGroup }

    Get-EalaDataCollectionEndpoint @param | ForEach-Object {
        @{
            Text    = $_.Name
            Tooltip = '{0} > {1} > {2} ({3})' -f $_.Subscription, $_.ResourceGroup, $_.Name, $_.Location
        }
    }
}
Register-PSFTeppScriptblock -Name 'EntraAuth.Azure.LogAnalytics.DataCollectionRule' -ScriptBlock {
    if (-not $fakeBoundParameter.Subscription) { return }
    $param = @{ Subscription = $fakeBoundParameter.Subscription }
    if ($fakeBoundParameter.ResourceGroup) { $param.ResourceGroup = $fakeBoundParameter.ResourceGroup }

    Get-EalaDataCollectionRule @param | ForEach-Object {
        @{
            Text    = $_.Name
            Tooltip = '{0} > {1} > {2} ({3})' -f $_.Subscription, $_.ResourceGroup, $_.Name, $_.Location
        }
    }
}

Set-PSFScriptblock -Name 'EntraAuth.Azure.LogAnalytics.TableColumnValidation' -Scriptblock {
    $legalTypes = 'string', 'int', 'long', 'real', 'boolean', 'dateTime', 'guid', 'dynamic'
    $legalKeys = 'dataTypeHint', 'description', 'displayName', 'isDefaultDisplay', 'isHidden', 'name', 'type'
    foreach ($entry in $_) {
        if ($entry -isnot [hashtable]) { throw 'Invalid column entry: Not a hashtable!' }
        if (-not $entry.Name) { throw "Invalid column entry: Must contain a 'name' key" }
        if (-not $entry.type) { throw "Invalid column entry $($entry.name): Must contain a 'type' key" }
        if ($entry.type -notin $legalTypes) { throw "Invalid column entry $($entry.name): Illegal type $($entry.type) | Supported types: $($legalTypes -join ', ') | https://learn.microsoft.com/en-us/rest/api/loganalytics/tables/create-or-update?view=rest-loganalytics-2026-03-01&tabs=HTTP#columntypeenum" }

        foreach ($key in $entry.Keys) {
            if ($key -notin $legalKeys) { throw "Invalid column property: $key | Legal Entries: $($legalKeys -join ', ') | https://learn.microsoft.com/en-us/rest/api/loganalytics/tables/create-or-update?view=rest-loganalytics-2026-03-01&tabs=HTTP#column" }
        }
    }
    $true
} -Global

$param = @{
    Name        = 'PSFrameworkLog'
    Description = 'Columns needed for PSFramework logging'
    Columns     = @(
        @{
            name = 'Message'
            type = 'string'
        }
        @{
            name = 'TimeGenerated'
            type = 'datetime'
        }
        @{
            name = 'Level'
            type = 'string'
        }
        @{
            name = 'Tags'
            type = 'dynamic'
        }
        @{
            name = 'Data'
            type = 'dynamic'
        }
        @{
            name = 'ComputerName'
            type = 'string'
        }
        @{
            name = 'Runspace'
            type = 'string'
        }
        @{
            name = 'Username'
            type = 'string'
        }
        @{
            name = 'ModuleName'
            type = 'string'
        }
        @{
            name = 'FunctionName'
            type = 'string'
        }
        @{
            name = 'File'
            type = 'string'
        }
        @{
            name = 'Line'
            type = 'int'
        }
        @{
            name = 'Callstack'
            type = 'string'
        }
        @{
            name = 'TargetObject'
            type = 'dynamic'
        }
        @{
            name = 'ErrorRecord'
            type = 'dynamic'
        }
    )
}
Register-EalaColumnTemplate @param

Export-ModuleMember -Function 'Get-EalaColumnTemplate','Get-EalaDataCollectionEndpoint','Get-EalaDataCollectionRule','Get-EalaTable','Get-EalaWorkspace','New-EalaDataCollectionEndpoint','New-EalaDataCollectionRule','New-EalaTable','New-EalaWorkspace','Register-EalaColumnTemplate','Remove-EalaDataCollectionEndpoint','Remove-EalaDataCollectionRule','Remove-EalaTable','Remove-EalaWorkspace','Set-EalaWorkspace','Write-EalaTableEntry'