Edit

Express configuration PowerShell wrapper module

Important

Updated to reflect SQL vulnerability assessment unified API reference that can be found under:

This article provides the PowerShell wrapper for SQL vulnerability assessment express configuration.

We recommend copying the script to a local file with the following file name SqlVulnerabilityAssessmentCommands.psm1.

You should use the Express configuration PowerShell commands reference to run the script.

SqlVulnerabilityAssessmentCommands.psm1

# =================================================================================
#  SqlVulnerabilityAssessmentCommands.psm1
#  ---------------------------------------------------------------------------------
#  Resource-agnostic PowerShell wrapper for the SQL Vulnerability Assessment (VA)
#  V20260401 REST API (api-version = 2026-04-01-preview).
#
#  Design
#  ------
#  Every public function takes a single `-ResourceId` parameter that locates the
#  target resource in ARM. The function appends the appropriate VA operation
#  segment under the `Microsoft.Security/sqlVulnerabilityAssessments/default`
#  provider namespace and issues the call via `Invoke-AzRestMethod`.
#
#  * Settings (Get / Set / Remove)            → `-ResourceId` is the
#    **top-level PaaS resource** (server / managed instance / Synapse workspace).
#    Settings are PaaS-only and are not supported on VM or Arc.
#
#  * Invoke-Scan, Get-ScanOperationResult, and Get-Scan → `-ResourceId`
#    is the **PaaS database** (Azure SQL DB / MI database / Synapse SQL pool).
#    Scan initiation, polling, and scan-record reads (single + list) are
#    PaaS-only and are not supported on VM or Arc. To read scan data on
#    IaaS, use `Get-ScanResult -ScanId latest` instead.
#
#  * Get-ScanResult, Baseline, BaselineRule → `-ResourceId` is the
#    **database resource id** for any of the five supported providers, including
#    the `sqlServers/{srv}/databases/{db}` child path on VM/Arc.
#
#  No provider-shape table is needed because the caller supplies the fully
#  qualified resource id; the module simply uses it as the route prefix.
#
#  All five providers in the V20260401 supported set are covered:
#      Microsoft.Sql/servers/{srv}/databases/{db}
#      Microsoft.Sql/managedInstances/{mi}/databases/{db}
#      Microsoft.Synapse/workspaces/{ws}/sqlPools/{pool}
#      Microsoft.Compute/virtualMachines/{vm}/sqlServers/{srv}/databases/{db}
#      Microsoft.HybridCompute/machines/{m}/sqlServers/{srv}/databases/{db}
#
#  System / master database resource ids (PaaS)
#  --------------------------------------------
#  How you express a system database (`master`, `model`, `msdb`) depends on
#  the PaaS provider:
#
#  * Azure SQL DB (`Microsoft.Sql/servers`) - system DBs are first-class
#    database resources. Use the regular database resource id, e.g.
#        …/servers/{srv}/databases/master
#    Every data-plane operation in this module works as-is.
#
#  * Managed Instance (`Microsoft.Sql/managedInstances`) - the system DBs
#    `master`, `model`, and `msdb` are valid VA targets but must be addressed
#    on the instance-level URL with `databaseName` as a query parameter.
#    Callers still pass the natural database resource id, e.g.
#        …/managedInstances/{mi}/databases/master
#    The module detects the system-DB name and transparently rewrites the
#    request to `…/managedInstances/{mi}/providers/Microsoft.Security/sqlVulnerabilityAssessments/default/…?api-version=…&databaseName=master`.
#
#  * Synapse (`Microsoft.Synapse/workspaces`) - the only system DB exposed is
#    `master` (on the serverless SQL endpoint). Address it as
#        …/workspaces/{ws}/sqlPools/master
#    The module detects this and routes to the workspace-level URL with
#    `databaseName=master`. Regular dedicated pools continue to use the
#    database-route as before.
# =================================================================================

Set-StrictMode -Version 3.0

#region Module constants

$Script:ApiVersion              = '2026-04-01-preview'
$Script:VaProviderSegment       = 'providers/Microsoft.Security/sqlVulnerabilityAssessments/default'
$Script:DefaultPollIntervalSec  = 5
$Script:DefaultTimeoutSec       = 600

#endregion

#region Internal helpers

function Test-VaResourceId {
    <#
        .SYNOPSIS
        Validates that a string looks like an ARM resource id.

        .DESCRIPTION
        Performs lightweight structural validation only - confirms that the
        value starts with `/subscriptions/{guid}/resourceGroups/{name}/providers/`.
        The RP enforces provider/route shape on the wire.
    #>
    param([Parameter(Mandatory)][string] $ResourceId)
    if ($ResourceId -notmatch '^/subscriptions/[0-9a-fA-F-]{36}/resourceGroups/[^/]+/providers/[^/]+/[^/]+/.+') {
        throw "Invalid -ResourceId '$ResourceId'. Expected an ARM resource id of the form '/subscriptions/{guid}/resourceGroups/{rg}/providers/{ns}/{type}/{name}[/...]'."
    }
}

function Test-VaPaasOnly {
    <#
        .SYNOPSIS
        Throws when the resource id targets an IaaS host (VM or Arc machine).

        .DESCRIPTION
        A subset of V20260401 operations - Settings (Get/Set/Remove),
        InitiateScan, Get-ScanOperationResult, and **Get-Scan (single + list)**
        - are supported only on PaaS hosts (Microsoft.Sql/servers,
        Microsoft.Sql/managedInstances, Microsoft.Synapse/workspaces). This
        helper inspects the provider segment of the ARM resource id and
        throws a descriptive error if the caller passes a
        `Microsoft.Compute/virtualMachines` or
        `Microsoft.HybridCompute/machines` resource id.

        Called only by the 6 PaaS-only functions; all other functions
        (Get-ScanResult, Get/Add baseline, Get/Set/Remove BaselineRule)
        accept any of the 5 supported provider types.
    #>
    param([Parameter(Mandatory)][string] $ResourceId)
    if ($ResourceId -match '/providers/(Microsoft\.Compute/virtualMachines|Microsoft\.HybridCompute/machines)/') {
        throw "This operation is not supported for VM (Microsoft.Compute/virtualMachines) or Arc (Microsoft.HybridCompute/machines) resources. Settings, scan-initiation/polling, and scan-record reads are PaaS-only (Microsoft.Sql/servers, Microsoft.Sql/managedInstances, Microsoft.Synapse/workspaces). Use Get-SqlVulnerabilityAssessmentScanResult -ScanId latest to read scan data on IaaS."
    }
}

function Resolve-VaRoute {
    <#
        .SYNOPSIS
        Resolves the effective routing URL components for a given `-ResourceId`.

        .DESCRIPTION
        Most resource ids map 1:1 to the request URL (the database appears in
        the ARM path). The V20260401 RP, however, requires a *server-level*
        URL plus a `databaseName` query parameter for:

            * Managed Instance system DBs : `master`, `model`, `msdb`
            * Synapse `master` (the serverless `sqlPools/master`)

        This helper detects those patterns and returns the rewritten route id
        plus the database name to inject into the query string. For every
        other resource id it returns the original id and a null database name.

        Returns a `[pscustomobject]` with `RouteId` and `DatabaseName`.
    #>
    param([Parameter(Mandatory)][string] $ResourceId)

    $trimmed = $ResourceId.TrimEnd('/')

    # Managed Instance system DBs → server route on the instance.
    if ($trimmed -match '^(?<srv>.+/providers/Microsoft\.Sql/managedInstances/[^/]+)/databases/(?<db>master|model|msdb)$') {
        return [pscustomobject]@{ RouteId = $Matches.srv; DatabaseName = $Matches.db }
    }

    # Synapse master (serverless) → server route on the workspace.
    if ($trimmed -match '^(?<srv>.+/providers/Microsoft\.Synapse/workspaces/[^/]+)/sqlPools/(?<db>master)$') {
        return [pscustomobject]@{ RouteId = $Matches.srv; DatabaseName = $Matches.db }
    }

    return [pscustomobject]@{ RouteId = $trimmed; DatabaseName = $null }
}

function Get-VaUri {
    <#
        .SYNOPSIS
        Builds the full VA REST URI for an operation against a target resource.

        .DESCRIPTION
        Combines the caller's `-ResourceId`, the VA provider segment, an
        optional operation suffix (e.g. `/scans/initiateScan`), and the
        api-version query string.

        For MI system DBs (`master`, `model`, `msdb`) and Synapse `master`,
        the URL is rewritten to the server-level form with a `databaseName`
        query parameter - see `Resolve-VaRoute` for details.
    #>
    param(
        [Parameter(Mandatory)][string] $ResourceId,
        [string] $OperationSuffix = '',
        [string] $ApiVersion = $Script:ApiVersion
    )
    Test-VaResourceId -ResourceId $ResourceId
    $route = Resolve-VaRoute -ResourceId $ResourceId
    $qs    = "?api-version=$ApiVersion"
    if ($route.DatabaseName) { $qs += "&databaseName=$($route.DatabaseName)" }
    return "$($route.RouteId)/$($Script:VaProviderSegment)$OperationSuffix$qs"
}

function SendRestRequest {
    <#
        .SYNOPSIS
        Thin wrapper around `Invoke-AzRestMethod` that throws on HTTP failure.

        .DESCRIPTION
        Submits the request, parses the JSON response when present, and throws
        a descriptive error for any non-2xx status code. Returns the raw
        response object so callers that need the headers (e.g. Location for
        async operations) or the status code can still inspect them.
    #>
    param(
        [Parameter(Mandatory)][ValidateSet('Get','Put','Post','Delete')][string] $Method,
        [Parameter(Mandatory)][string] $Uri,
        [string] $Body
    )

    $params = @{
        Method = $Method.ToUpperInvariant()
        Path   = $Uri
    }
    if ($PSBoundParameters.ContainsKey('Body') -and -not [string]::IsNullOrEmpty($Body)) {
        $params['Payload'] = $Body
    }

    $resp = Invoke-AzRestMethod @params
    if (-not $resp) {
        throw "Invoke-AzRestMethod returned no response for $Method $Uri."
    }
    if ($resp.StatusCode -lt 200 -or $resp.StatusCode -ge 300) {
        throw "HTTP $($resp.StatusCode) calling $Method $Uri. Body: $($resp.Content)"
    }
    return $resp
}

function Get-VaLocationHeader {
    <#
        .SYNOPSIS
        Extracts the Location response header in a shape-tolerant way.

        .DESCRIPTION
        `Invoke-AzRestMethod` surfaces Headers as either a Hashtable (with
        string-array values) or an IEnumerable of KeyValuePair objects,
        depending on PowerShell version and Az module version. This helper
        normalises both shapes and returns the first Location value as a
        single string (or $null if absent).
    #>
    param([Parameter(Mandatory)] $Response)
    if (-not $Response.Headers) { return $null }
    $h = $Response.Headers
    if ($h -is [System.Collections.IDictionary]) {
        if ($h.Contains('Location')) {
            $v = $h['Location']
            if ($v -is [System.Collections.IEnumerable] -and -not ($v -is [string])) {
                return ($v | Select-Object -First 1)
            }
            return [string]$v
        }
        return $null
    }
    $kv = $h | Where-Object { $_.Key -eq 'Location' } | Select-Object -First 1
    if (-not $kv) { return $null }
    $val = $kv.Value
    if ($val -is [System.Collections.IEnumerable] -and -not ($val -is [string])) {
        return ($val | Select-Object -First 1)
    }
    return [string]$val
}

function Wait-VaScanOperation {
    <#
        .SYNOPSIS
        Polls a scan operation URL until it reaches a terminal state.

        .DESCRIPTION
        Issues GETs against `-OperationUri` at `-PollIntervalSeconds`
        cadence. The operation is terminal when `properties.scanStatus` is
        one of `Passed`, `Failed`, or `FailedToRun`. Throws after
        `-TimeoutSeconds` elapsed. Returns the terminal scan status string.
    #>
    param(
        [Parameter(Mandatory)][string] $OperationUri,
        [int] $PollIntervalSeconds = $Script:DefaultPollIntervalSec,
        [int] $TimeoutSeconds      = $Script:DefaultTimeoutSec
    )

    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    while ($true) {
        $resp = SendRestRequest -Method 'Get' -Uri $OperationUri
        $obj = $resp.Content | ConvertFrom-Json
        $status = $obj.properties.scanStatus
        if ($status -in @('Passed', 'Failed', 'FailedToRun')) {
            return [string]$status
        }
        if ((Get-Date) -ge $deadline) {
            throw "Timed out after $TimeoutSeconds second(s) waiting for scan operation to complete. Last status: '$status'."
        }
        if ($PollIntervalSeconds -gt 0) {
            Start-Sleep -Seconds $PollIntervalSeconds
        }
    }
}

#endregion

###################################################################################
###  Settings - top-level (server / MI / workspace ) VA policy        ###
###################################################################################

function Get-SqlVulnerabilityAssessmentSetting {
    <#
        .SYNOPSIS
        Gets the VA settings for a top-level resource.

        .DESCRIPTION
        Wraps `GET {resourceId}/providers/Microsoft.Security/sqlVulnerabilityAssessments/default`.
        Returns 200 with the settings document; `properties.state` is
        `Enabled` or `Disabled` (the RP returns `Disabled` when VA has
        never been configured on the resource - there is no 404 case).

        .PARAMETER ResourceId
        ARM resource id of the **top-level** PaaS resource:
        SQL server (`Microsoft.Sql/servers`), Managed Instance
        (`Microsoft.Sql/managedInstances`), or Synapse workspace
        (`Microsoft.Synapse/workspaces`).

        VM (`Microsoft.Compute/virtualMachines`) and Arc
        (`Microsoft.HybridCompute/machines`) are **not supported** for
        Settings operations.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        Get-SqlVulnerabilityAssessmentSetting `
            -ResourceId '/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/rg/providers/Microsoft.Sql/servers/srv'
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        Test-VaPaasOnly -ResourceId $ResourceId
        $uri = Get-VaUri -ResourceId $ResourceId -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Get' -Uri $uri
    }
}

function Set-SqlVulnerabilityAssessmentSetting {
    <#
        .SYNOPSIS
        Creates or updates VA settings on a top-level resource.

        .DESCRIPTION
        Wraps `PUT {resourceId}/providers/Microsoft.Security/sqlVulnerabilityAssessments/default`.
        The function constructs the request body from the `-State` parameter
        (`Enabled` or `Disabled`) - callers do not need to craft JSON.

        .PARAMETER ResourceId
        ARM resource id of the top-level **PaaS** resource (server / MI /
        workspace). VM and Arc are **not supported**.

        .PARAMETER State
        Desired VA state. One of `Enabled` or `Disabled`.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        Set-SqlVulnerabilityAssessmentSetting -ResourceId $sqlSrv.ResourceId -State Enabled

        .EXAMPLE
        Set-SqlVulnerabilityAssessmentSetting -ResourceId $sqlSrv.ResourceId -State Disabled
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [Parameter(Mandatory)][ValidateSet('Enabled','Disabled')][string] $State,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        Test-VaPaasOnly -ResourceId $ResourceId
        $uri  = Get-VaUri -ResourceId $ResourceId -ApiVersion $ApiVersion
        $body = @{ properties = @{ state = $State } } | ConvertTo-Json -Depth 4 -Compress
        return SendRestRequest -Method 'Put' -Uri $uri -Body $body
    }
}

function Remove-SqlVulnerabilityAssessmentSetting {
    <#
        .SYNOPSIS
        Deletes the VA settings from a top-level resource.

        .DESCRIPTION
        Wraps `DELETE {resourceId}/providers/Microsoft.Security/sqlVulnerabilityAssessments/default`.

        .PARAMETER ResourceId
        ARM resource id of the top-level **PaaS** resource (server / MI /
        workspace). VM and Arc are **not supported**.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        Remove-SqlVulnerabilityAssessmentSetting -ResourceId $sqlSrv.ResourceId
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        Test-VaPaasOnly -ResourceId $ResourceId
        $uri = Get-VaUri -ResourceId $ResourceId -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Delete' -Uri $uri
    }
}

###################################################################################
###  Scans - initiate, poll, get/list scan records                              ###
###################################################################################

function Invoke-SqlVulnerabilityAssessmentScan {
    <#
        .SYNOPSIS
        Initiates a VA scan on a database resource.

        .DESCRIPTION
        Wraps `POST {dbResourceId}/.../scans/initiateScan`. The RP responds
        with HTTP 202 + a `Location` header pointing at the scan operation.

        Without `-Wait`, returns an object containing the initial status code,
        the raw Location, and a parsed `OperationId` so callers can poll
        themselves via `Get-SqlVulnerabilityAssessmentScanOperationResult`.

        With `-Wait`, the function polls the operation URL until it reaches
        a terminal state (`Passed`, `Failed`, `FailedToRun`) and then issues
        `GET .../scans/latest` to return the final scan record properties
        (trigger type, scan status, start/end time, server, database,
        rule counts, baseline applied flag, …).

        .PARAMETER ResourceId
        ARM resource id of the **database** (Microsoft.Sql/servers/.../databases
        or Microsoft.Sql/managedInstances/.../databases or
        Microsoft.Synapse/workspaces/.../sqlPools).

        Scan initiation and polling are **PaaS-only**. VM
        (`Microsoft.Compute/virtualMachines`) and Arc
        (`Microsoft.HybridCompute/machines`) resources are not supported by
        this operation - the function throws a descriptive error if such a
        resource id is supplied.

        .PARAMETER Wait
        If specified, polls the scan operation until it completes.

        .PARAMETER PollIntervalSeconds
        Seconds between polling GETs when `-Wait` is set. Defaults to 5.

        .PARAMETER TimeoutSeconds
        Maximum seconds to wait for the scan to complete. Defaults to 600.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        # Fire-and-forget
        $r = Invoke-SqlVulnerabilityAssessmentScan -ResourceId $dbId
        $r.OperationId

        .EXAMPLE
        # Wait for completion
        $r = Invoke-SqlVulnerabilityAssessmentScan -ResourceId $dbId -Wait
        $r.ScanStatus     # 'Passed' / 'Failed' / 'FailedToRun'
        $r.Scan           # Full scan record (triggerType, state, startTime, endTime, …)
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [switch] $Wait,
        [int] $PollIntervalSeconds = $Script:DefaultPollIntervalSec,
        [int] $TimeoutSeconds      = $Script:DefaultTimeoutSec,
        [string] $ApiVersion       = $Script:ApiVersion
    )
    process {
        Test-VaPaasOnly -ResourceId $ResourceId
        $uri  = Get-VaUri -ResourceId $ResourceId -OperationSuffix '/scans/initiateScan' -ApiVersion $ApiVersion
        $resp = SendRestRequest -Method 'Post' -Uri $uri

        $location = Get-VaLocationHeader -Response $resp

        if (-not $Wait) {
            $operationId = $null
            if ($location -and ($location -match '/scanOperationResults/([^/?]+)')) {
                $operationId = $Matches[1]
            }
            return [pscustomobject]@{
                StatusCode  = $resp.StatusCode
                Location    = $location
                OperationId = $operationId
                Response    = $resp
            }
        }

        if (-not $location) {
            throw "InitiateScan returned status $($resp.StatusCode) with no Location header to poll."
        }

        # Strip the host prefix; Invoke-AzRestMethod's -Path accepts either form.
        $pollPath = $location
        if ($pollPath -match '^https?://[^/]+(/.+)$') { $pollPath = $Matches[1] }

        $terminalStatus = Wait-VaScanOperation -OperationUri $pollPath -PollIntervalSeconds $PollIntervalSeconds -TimeoutSeconds $TimeoutSeconds

        # Once the operation completes, fetch the latest scan record so the
        # caller gets the full scan summary (triggerType, state, startTime,
        # endTime, scan/rule counts, baseline-applied flag, …), not just
        # the scanOperationResults envelope which only carries scanStatus.
        $scanUri  = Get-VaUri -ResourceId $ResourceId -OperationSuffix '/scans/latest' -ApiVersion $ApiVersion
        $scanResp = SendRestRequest -Method 'Get' -Uri $scanUri
        $scanObj  = $scanResp.Content | ConvertFrom-Json

        return [pscustomobject]@{
            ScanStatus = $terminalStatus
            Scan       = $scanObj
        }
    }
}

function Get-SqlVulnerabilityAssessmentScanOperationResult {
    <#
        .SYNOPSIS
        Polls the result of a previously initiated VA scan.

        .DESCRIPTION
        Wraps `GET {dbResourceId}/.../scans/scanOperationResults/{operationId}`.
        Use the operation id returned by `Invoke-SqlVulnerabilityAssessmentScan`
        to poll the scan's status.

        Terminal states are `Passed`, `Failed`, `FailedToRun`. The only
        non-terminal state is `InProgress`.

        .PARAMETER ResourceId
        ARM resource id of the **PaaS database** resource. Scan polling is
        PaaS-only - VM and Arc resource ids throw a descriptive error.

        .PARAMETER OperationId
        Operation id returned by `Invoke-SqlVulnerabilityAssessmentScan`.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        Get-SqlVulnerabilityAssessmentScanOperationResult -ResourceId $dbId -OperationId 'op-abc'
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [Parameter(Mandatory)][string] $OperationId,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        Test-VaPaasOnly -ResourceId $ResourceId
        $uri = Get-VaUri -ResourceId $ResourceId -OperationSuffix "/scans/scanOperationResults/$OperationId" -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Get' -Uri $uri
    }
}

function Get-SqlVulnerabilityAssessmentScan {
    <#
        .SYNOPSIS
        Gets a single scan record or lists all scan records on a database.

        .DESCRIPTION
        Wraps `GET {dbResourceId}/.../scans` (LIST) or
        `GET {dbResourceId}/.../scans/{scanId}` (GET). Pass `-ScanId 'latest'`
        to retrieve the most recent completed scan.

        Supported on PaaS providers only (Microsoft.Sql/servers,
        Microsoft.Sql/managedInstances, Microsoft.Synapse/workspaces). Scan
        records on VM and Arc are not exposed through the unified RP - use
        `Get-SqlVulnerabilityAssessmentScanResult -ScanId latest` to read
        scan data on IaaS instead.

        .PARAMETER ResourceId
        ARM resource id of the database resource.

        .PARAMETER ScanId
        Optional scan id; when omitted the function lists all scans.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        Get-SqlVulnerabilityAssessmentScan -ResourceId $dbId
        Get-SqlVulnerabilityAssessmentScan -ResourceId $dbId -ScanId 'latest'
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [string] $ScanId,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        Test-VaPaasOnly -ResourceId $ResourceId
        $suffix = if ($ScanId) { "/scans/$ScanId" } else { '/scans' }
        $uri = Get-VaUri -ResourceId $ResourceId -OperationSuffix $suffix -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Get' -Uri $uri
    }
}

###################################################################################
###  Scan Results                                                               ###
###################################################################################

function Get-SqlVulnerabilityAssessmentScanResult {
    <#
        .SYNOPSIS
        Gets a single scan result (rule outcome) or lists all rule results
        for a given scan.

        .DESCRIPTION
        Wraps `GET {dbResourceId}/.../scans/{scanId}/scanResults` (LIST) or
        `GET {dbResourceId}/.../scans/{scanId}/scanResults/{ruleId}` (GET).
        `-ScanId 'latest'` is supported.

        .PARAMETER ResourceId
        ARM resource id of the database resource.

        .PARAMETER ScanId
        Scan id (or `latest`) whose results to fetch.

        .PARAMETER RuleId
        Optional rule id; when omitted the function lists all rule results.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        Get-SqlVulnerabilityAssessmentScanResult -ResourceId $dbId -ScanId 'latest'
        Get-SqlVulnerabilityAssessmentScanResult -ResourceId $dbId -ScanId 'latest' -RuleId 'VA2065'
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [Parameter(Mandatory)][string] $ScanId,
        [string] $RuleId,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        $suffix = if ($RuleId) { "/scans/$ScanId/scanResults/$RuleId" } else { "/scans/$ScanId/scanResults" }
        $uri = Get-VaUri -ResourceId $ResourceId -OperationSuffix $suffix -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Get' -Uri $uri
    }
}

###################################################################################
###  Baselines and Baseline Rules                                               ###
###################################################################################

function Get-SqlVulnerabilityAssessmentBaseline {
    <#
        .SYNOPSIS
        Lists all baseline rules configured on a database.

        .DESCRIPTION
        Wraps `GET {dbResourceId}/.../baselineRules`. Supports any of the
        five V20260401 provider types (Sql/servers/databases,
        Sql/managedInstances/databases, Synapse/workspaces/sqlPools,
        Compute/virtualMachines/sqlServers/databases,
        HybridCompute/machines/sqlServers/databases).

        .PARAMETER ResourceId
        ARM resource id of the database resource.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        Get-SqlVulnerabilityAssessmentBaseline -ResourceId $dbId
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        $uri = Get-VaUri -ResourceId $ResourceId -OperationSuffix '/baselineRules' -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Get' -Uri $uri
    }
}

function Add-SqlVulnerabilityAssessmentBaseline {
    <#
        .SYNOPSIS
        Bulk-adds baseline rules on a database.

        .DESCRIPTION
        Wraps `POST {dbResourceId}/.../baselineRules`. The `-Body`
        parameter is a JSON document containing the rules to add. Use the
        `latestScan` action to seed the baseline from the most recent scan
        results.

        .PARAMETER ResourceId
        ARM resource id of the database resource.

        .PARAMETER Body
        JSON request body for the bulk-add operation.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        # Seed the baseline from the latest scan results (recommended).
        Add-SqlVulnerabilityAssessmentBaseline -ResourceId $dbId -Body '{"latestScan":true}'

        .EXAMPLE
        # Seed an explicit baseline (flat body - no `properties` wrapper).
        $body = @{ latestScan = $false; results = @{ VA2065 = @(,@('dbo','db_owner')) } } | ConvertTo-Json -Depth 6
        Add-SqlVulnerabilityAssessmentBaseline -ResourceId $dbId -Body $body
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [Parameter(Mandatory)][string] $Body,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        $uri = Get-VaUri -ResourceId $ResourceId -OperationSuffix '/baselineRules' -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Post' -Uri $uri -Body $Body
    }
}

function Get-SqlVulnerabilityAssessmentBaselineRule {
    <#
        .SYNOPSIS
        Gets a single baseline rule from a database.

        .DESCRIPTION
        Wraps `GET {dbResourceId}/.../baselineRules/{ruleId}`.

        .PARAMETER ResourceId
        ARM resource id of the database resource.

        .PARAMETER RuleId
        Baseline rule id (e.g. `VA2065`).

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        Get-SqlVulnerabilityAssessmentBaselineRule -ResourceId $dbId -RuleId 'VA2065'
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [Parameter(Mandatory)][string] $RuleId,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        $uri = Get-VaUri -ResourceId $ResourceId -OperationSuffix "/baselineRules/$RuleId" -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Get' -Uri $uri
    }
}

function Set-SqlVulnerabilityAssessmentBaselineRule {
    <#
        .SYNOPSIS
        Creates or updates a single baseline rule.

        .DESCRIPTION
        Wraps `PUT {dbResourceId}/.../baselineRules/{ruleId}`.

        .PARAMETER ResourceId
        ARM resource id of the database resource.

        .PARAMETER RuleId
        Baseline rule id.

        .PARAMETER Body
        JSON request body describing the rule's expected results.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        # PUT body is FLAT (no `properties` wrapper). `latestScan` is required.
        $body = @{ latestScan = $false; results = @(,@('user1','db_owner')) } | ConvertTo-Json -Depth 5
        Set-SqlVulnerabilityAssessmentBaselineRule -ResourceId $dbId -RuleId 'VA2065' -Body $body
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [Parameter(Mandatory)][string] $RuleId,
        [Parameter(Mandatory)][string] $Body,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        $uri = Get-VaUri -ResourceId $ResourceId -OperationSuffix "/baselineRules/$RuleId" -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Put' -Uri $uri -Body $Body
    }
}

function Remove-SqlVulnerabilityAssessmentBaselineRule {
    <#
        .SYNOPSIS
        Deletes a single baseline rule from a database.

        .DESCRIPTION
        Wraps `DELETE {dbResourceId}/.../baselineRules/{ruleId}`.

        .PARAMETER ResourceId
        ARM resource id of the database resource.

        .PARAMETER RuleId
        Baseline rule id.

        .PARAMETER ApiVersion
        REST API version. Defaults to `2026-04-01-preview`.

        .EXAMPLE
        Remove-SqlVulnerabilityAssessmentBaselineRule -ResourceId $dbId -RuleId 'VA2065'
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)][string] $ResourceId,
        [Parameter(Mandatory)][string] $RuleId,
        [string] $ApiVersion = $Script:ApiVersion
    )
    process {
        $uri = Get-VaUri -ResourceId $ResourceId -OperationSuffix "/baselineRules/$RuleId" -ApiVersion $ApiVersion
        return SendRestRequest -Method 'Delete' -Uri $uri
    }
}

###################################################################################
###  Exports                                                                    ###
###################################################################################

Export-ModuleMember -Function @(
    # Settings
    'Get-SqlVulnerabilityAssessmentSetting',
    'Set-SqlVulnerabilityAssessmentSetting',
    'Remove-SqlVulnerabilityAssessmentSetting',
    # Scans
    'Invoke-SqlVulnerabilityAssessmentScan',
    'Get-SqlVulnerabilityAssessmentScan',
    'Get-SqlVulnerabilityAssessmentScanOperationResult',
    # Scan results
    'Get-SqlVulnerabilityAssessmentScanResult',
    # Baselines
    'Get-SqlVulnerabilityAssessmentBaseline',
    'Add-SqlVulnerabilityAssessmentBaseline',
    'Get-SqlVulnerabilityAssessmentBaselineRule',
    'Set-SqlVulnerabilityAssessmentBaselineRule',
    'Remove-SqlVulnerabilityAssessmentBaselineRule'
)

Next steps

Express configuration PowerShell commands reference.