Private/ConvertFrom-CustomDetectionJsonToYaml.ps1

function ConvertFrom-CustomDetectionJsonToYaml {
    <#
    .SYNOPSIS
        Converts a Graph API detection rule into the YAML rule shape.
 
    .DESCRIPTION
        Emits the current YAML keys (tactics, entityMappings, customDetails,
        ISO 8601 frequency, description). Rules that still carry the legacy
        properties (category, mitreTechniques, impactedAssets, responseActions,
        period, isEnabled) are translated so the output uses the current keys.
        isEnabled is always emitted and status only for autoDisabled. Read-only
        and runtime properties are dropped.
 
        lookback, eventGroupingKind and isExcludedFromIncidentCorrelation are
        emitted only when they differ from the value the API assigns: the
        documented lookback for the frequency, alertPerEvent and false.
        -KeepDefaults emits them whenever the rule carries them, so a deploy
        from JSON sends a default the file states.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [PSObject]$JsonObject,

        [Parameter()]
        [bool]$SetEnabled,

        [Parameter()]
        [ValidateSet('Informational', 'Low', 'Medium', 'High')]
        [string]$SetSeverity,

        [Parameter()]
        [switch]$ValidateIdentifiers,

        [Parameter()]
        [switch]$KeepDefaults
    )

    $DefaultSortOrderInYAML = @(
        'guid'
        'detectorId'
        'ruleName'
        'description'
        'isEnabled'
        'status'
        'alertTitle'
        'frequency'
        'lookback'
        'alertSeverity'
        'alertDescription'
        'alertRecommendedAction'
        'eventGroupingKind'
        'tactics'
        'entityMappings'
        'customDetails'
        'organizationalScope'
        'isExcludedFromIncidentCorrelation'
        'actions'
        'queryText'
    )

    # Strip the description tag from the alert description
    $uuidPattern = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
    $alertDescription = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.description'
    if ($alertDescription) {
        $alertDescription = ($alertDescription -replace "\s*\[(?:[^:\]]*:)?$uuidPattern\]", '').Trim()
    }

    # Keep the raw status here so an autoDisabled rule exports as such; the forward path maps it on deploy
    $rawStatus = "$(Get-CustomDetectionValue -Object $JsonObject -Path 'status')".Trim()
    $status = if ($PSBoundParameters.ContainsKey('SetEnabled')) {
        ConvertTo-CustomDetectionStatus -IsEnabled $SetEnabled
    } elseif ($rawStatus -eq 'autoDisabled') {
        'autoDisabled'
    } else {
        ConvertTo-CustomDetectionStatus -IsEnabled (Get-CustomDetectionValue -Object $JsonObject -Path 'isEnabled') -Status $rawStatus
    }

    $severity = if ($PSBoundParameters.ContainsKey('SetSeverity')) {
        $SetSeverity
    } else {
        $rawSeverity = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.severity'
        if ($rawSeverity) { (Get-Culture).TextInfo.ToTitleCase("$rawSeverity".ToLowerInvariant()) } else { $null }
    }

    $yamlObj = [ordered]@{
        guid             = Get-CustomDetectionIdentity -Rule $JsonObject
        ruleName         = Get-CustomDetectionValue -Object $JsonObject -Path 'displayName'
        isEnabled        = ($status -eq 'enabled')
        alertTitle       = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.title'
        alertSeverity    = $severity
        alertDescription = $alertDescription
        queryText        = Get-CustomDetectionValue -Object $JsonObject -Path 'queryCondition.queryText'
    }

    if ($status -notin @('enabled', 'disabled')) {
        $yamlObj['status'] = $status
    }

    $description = Get-CustomDetectionValue -Object $JsonObject -Path 'description'
    if (Test-CustomDetectionValue $description) {
        $yamlObj['description'] = $description
    }

    $frequencySource = Get-CustomDetectionValue -Object $JsonObject -Path 'schedule.frequency'
    if (-not (Test-CustomDetectionValue $frequencySource)) {
        $frequencySource = Get-CustomDetectionValue -Object $JsonObject -Path 'schedule.period'
    }
    if (Test-CustomDetectionValue $frequencySource) {
        $yamlObj['frequency'] = ConvertTo-CustomDetectionFrequency -Value $frequencySource
    }

    # The lookback the API assigns to each frequency when a request sets none. A near-real-time rule has none
    $defaultLookback = @{ 'PT1H' = 'PT4H'; 'PT3H' = 'PT12H'; 'PT12H' = 'P2D'; 'P1D' = 'P30D' }
    $lookbackSource = Get-CustomDetectionValue -Object $JsonObject -Path 'schedule.lookback'
    if (Test-CustomDetectionValue $lookbackSource) {
        $lookback = ConvertTo-CustomDetectionLookback -Value $lookbackSource
        if ($KeepDefaults -or $lookback -ne $defaultLookback["$($yamlObj['frequency'])"]) {
            $yamlObj['lookback'] = $lookback
        }
    }

    $recommendedActions = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.recommendedActions'
    if (Test-CustomDetectionValue $recommendedActions) {
        $yamlObj['alertRecommendedAction'] = $recommendedActions
    }

    $groupingKind = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.eventGroupingKind'
    if ((Test-CustomDetectionValue $groupingKind) -and ($KeepDefaults -or "$groupingKind" -ne 'alertPerEvent')) {
        $yamlObj['eventGroupingKind'] = "$groupingKind"
    }

    $excluded = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.incidentConfiguration.isExcludedFromIncidentCorrelation'
    if ($null -ne $excluded -and "$excluded" -ne '' -and ($KeepDefaults -or "$excluded" -ne 'false')) {
        # The value keeps its type so the deploy-side validation can reject one that is not a boolean
        $yamlObj['isExcludedFromIncidentCorrelation'] = $excluded
    }

    $tacticsSource = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.tactics'
    $tactics = @()
    if (Test-CustomDetectionValue $tacticsSource) {
        $tactics = @(ConvertTo-CustomDetectionTactics -Tactics $tacticsSource)
    } else {
        $category = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.category'
        if (Test-CustomDetectionValue $category) {
            $techniques = @(Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.mitreTechniques' | Where-Object { $_ })
            $tactics = @(ConvertTo-CustomDetectionTactics -Category "$category" -Techniques $techniques)
        }
    }
    if ($tactics.Count -gt 0) {
        $yamlObj['tactics'] = [object[]]$tactics
    }

    $entityMappingsSource = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.entityMappings'
    $entityMappings = $null
    if (Test-CustomDetectionCollection -Value $entityMappingsSource) {
        $entityMappings = ConvertFrom-CustomDetectionEntityMappings -EntityMappings $entityMappingsSource -ValidateIdentifiers:$ValidateIdentifiers
    } else {
        $impactedAssets = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.impactedAssets'
        if (Test-CustomDetectionValue $impactedAssets) {
            $entityMappings = ConvertFrom-CustomDetectionEntityMappings -ImpactedAssets @($impactedAssets) -ValidateIdentifiers:$ValidateIdentifiers
        }
    }
    if ($entityMappings) {
        $yamlObj['entityMappings'] = $entityMappings
    }

    $customDetailsSource = ConvertTo-CustomDetectionHashtable -InputObject (Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.customDetails')
    if ($customDetailsSource -and $customDetailsSource.Count -gt 0) {
        $customDetails = [ordered]@{}
        foreach ($key in $customDetailsSource.Keys) {
            if (Test-CustomDetectionValue $customDetailsSource[$key]) {
                # The value keeps its type so the deploy-side validation can reject one that is not a string
                $customDetails[[string]$key] = $customDetailsSource[$key]
            }
        }
        if ($customDetails.Count -gt 0) {
            $yamlObj['customDetails'] = $customDetails
        }
    }

    $scopeSource = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.organizationalScope'
    $deviceGroups = @()
    if ($null -ne $scopeSource) {
        $scopeMap = ConvertTo-CustomDetectionHashtable -InputObject $scopeSource
        if ($null -ne $scopeMap) {
            if (Test-CustomDetectionValue $scopeMap['deviceGroups']) {
                $deviceGroups = @($scopeMap['deviceGroups'])
            } elseif (Test-CustomDetectionValue $scopeMap['scopeNames']) {
                $deviceGroups = @($scopeMap['scopeNames'])
            }
        } else {
            $deviceGroups = @($scopeSource | Where-Object { $_ })
        }
    }
    if ($deviceGroups.Count -gt 0) {
        # Entries keep their type so the deploy-side validation can reject a name that is not a string
        $yamlObj['organizationalScope'] = [object[]]$deviceGroups
    }

    $automatedActionsSource = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.automatedActions'
    $actions = @()
    if (Test-CustomDetectionCollection -Value $automatedActionsSource) {
        $actions = @(ConvertFrom-CustomDetectionAutomatedActions -AutomatedActions $automatedActionsSource)
    } else {
        $responseActions = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.responseActions'
        if (Test-CustomDetectionValue $responseActions) {
            $translated = ConvertTo-CustomDetectionAutomatedActions -ResponseActions @($responseActions)
            $actions = @(ConvertFrom-CustomDetectionAutomatedActions -AutomatedActions $translated)
        }
    }
    if ($actions.Count -gt 0) {
        $yamlObj['actions'] = [object[]]$actions
    }

    # The detector id is assigned by the API and travels with the file for reference
    $detectorId = Get-CustomDetectionValue -Object $JsonObject -Path 'detectorId'
    if (Test-CustomDetectionValue $detectorId) {
        $yamlObj['detectorId'] = "$detectorId"
    }

    $orderedYamlObj = ConvertTo-CustomDetectionOrderedMap -Map $yamlObj -Order $DefaultSortOrderInYAML

    return $orderedYamlObj
}