Private/ConvertFrom-CustomDetectionJsonToYaml.ps1

function ConvertFrom-CustomDetectionJsonToYaml {
    <#
    .SYNOPSIS
        Converts JSON content to YAML following the schema.
 
    .DESCRIPTION
        Performs the mapping from JSON properties back to YAML properties,
        omitting any properties not in the YAML schema.
    #>

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

        [Parameter()]
        [bool]$SetEnabled,

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

    $DefaultSortOrderInYAML = @(
        'guid'
        'isEnabled'
        'ruleName'
        'alertTitle'
        'alertCategory'
        'alertDescription'
        'frequency'
        'alertSeverity'
        'alertRecommendedAction'
        'mitreTechniques'
        'impactedEntities'
        'queryText'
    )

    # Extract DescriptionTag UUID from the description if present, preferring it over detectorId
    $descriptionTagGuid = $null
    $desc = $JsonObject.detectionAction.alertTemplate.description
    if ($desc) {
        $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}'
        $tagPattern = "\[(?:[^:\]]*:)?($uuidPattern)\]"
        if ($desc -match $tagPattern) {
            $descriptionTagGuid = $Matches[1]
        }
    }

    # Clean the description tag from the description text
    $cleanDescription = $desc
    if ($descriptionTagGuid) {
        $cleanDescription = ($desc -replace '\s*\[(?:[^:\]]*:)?[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}\]', '').Trim()
    }

    # Create YAML object with only schema-defined properties
    $yamlObj = @{
        guid             = if ($descriptionTagGuid) { $descriptionTagGuid } else { $JsonObject.detectorId }
        ruleName         = $JsonObject.displayName
        isEnabled        = if ($PSBoundParameters.ContainsKey('SetEnabled')) { $SetEnabled } else { $JsonObject.isEnabled }
        alertTitle       = $JsonObject.detectionAction.alertTemplate.title
        frequency        = [string]$JsonObject.schedule.period
        alertSeverity    = if ($PSBoundParameters.ContainsKey('SetSeverity')) { $SetSeverity } else { (Get-Culture).TextInfo.ToTitleCase($JsonObject.detectionAction.alertTemplate.severity) }
        alertDescription = $cleanDescription
        alertCategory    = $JsonObject.detectionAction.alertTemplate.category
        queryText        = $JsonObject.queryCondition.queryText
    }

    # Add optional properties if they exist
    if ($JsonObject.detectionAction.alertTemplate.recommendedActions) {
        $yamlObj.alertRecommendedAction = $JsonObject.detectionAction.alertTemplate.recommendedActions
    }

    if ($JsonObject.detectionAction.alertTemplate.mitreTechniques) {
        $yamlObj.mitreTechniques = $JsonObject.detectionAction.alertTemplate.mitreTechniques
    }

    # Map impactedAssets back to impactedEntities
    if ($JsonObject.detectionAction.alertTemplate.impactedAssets) {
        $impactedEntities = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($asset in $JsonObject.detectionAction.alertTemplate.impactedAssets) {
            # Extract entity type from @odata.type (e.g., "#microsoft.graph.security.impactedDeviceAsset" -> "Device")
            $odataType = $asset."@odata.type"
            if ($odataType -match 'impacted(\w+)Asset') {
                $entityType = $matches[1]
            }

            # Map Device back to Machine for YAML schema compliance
            $yamlEntityType = if ($entityType -eq 'Device') { 'Machine' } else { $entityType }

            $impactedEntities.Add(@{
                    entityType       = $yamlEntityType
                    entityIdentifier = $asset.identifier
                })
        }
        $yamlObj.impactedEntities = $impactedEntities
    }

    if ($JsonObject.detectionAction.organizationalScope) {
        $yamlObj.organizationalScope = $JsonObject.detectionAction.organizationalScope
    }

    if ($JsonObject.detectionAction.responseActions) {
        $yamlObj.actions = $JsonObject.detectionAction.responseActions
    }

    $orderedYamlObj = [ordered]@{}
    foreach ($key in $DefaultSortOrderInYAML) {
        if ($yamlObj.ContainsKey($key)) {
            $orderedYamlObj[$key] = $yamlObj[$key]
        }
    }

    foreach ($key in $yamlObj.Keys) {
        if (-not $orderedYamlObj.Contains($key)) {
            $orderedYamlObj[$key] = $yamlObj[$key]
        }
    }

    return $orderedYamlObj
}