Private/ConvertFrom-CustomDetectionYamlToJson.ps1

function ConvertFrom-CustomDetectionYamlToJson {
    <#
    .SYNOPSIS
        Converts YAML content to JSON following the Defender XDR schema.
 
    .DESCRIPTION
        Performs the mapping from YAML properties to JSON properties according to
        the Microsoft Defender XDR custom detection JSON schema.
    #>

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

        [Parameter()]
        [bool]$SetEnabled,

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

        [Parameter()]
        [switch]$SkipIdentifierValidation
    )

    # Start building the JSON object
    $jsonObj = @{
        detectionAction = @{
            alertTemplate       = @{}
            organizationalScope = $null
            responseActions     = @()
        }
        detectorId      = $YamlObject.guid
        displayName     = $YamlObject.ruleName
        isEnabled       = if ($PSBoundParameters.ContainsKey('SetEnabled')) { $SetEnabled } else { $YamlObject.isEnabled }
        queryCondition  = @{
            queryText = $YamlObject.queryText
        }
        schedule        = @{
            period = [string]$YamlObject.frequency
        }
    }

    # Map alertTemplate properties
    $jsonObj.detectionAction.alertTemplate.title = $YamlObject.alertTitle
    $jsonObj.detectionAction.alertTemplate.description = $YamlObject.alertDescription
    $jsonObj.detectionAction.alertTemplate.category = $YamlObject.alertCategory
    $jsonObj.detectionAction.alertTemplate.severity = if ($SetSeverity) { $SetSeverity.ToLower() } else { $YamlObject.alertSeverity.ToLower() }
    $jsonObj.detectionAction.alertTemplate.recommendedActions = $YamlObject.alertRecommendedAction

    # Map MITRE techniques
    if ($YamlObject.mitreTechniques) {
        $jsonObj.detectionAction.alertTemplate.mitreTechniques = $YamlObject.mitreTechniques
    }

    # Map impacted entities to impactedAssets
    if ($YamlObject.impactedEntities) {
        # Define valid identifiers for each asset type
        # https://learn.microsoft.com/en-us/graph/api/resources/security-impactedasset?view=graph-rest-beta
        $validIdentifiers = @{
            'Device'  = @(
                'deviceId', 'deviceName', 'remoteDeviceName', 'targetDeviceName', 'destinationDeviceName'
            )
            'User'    = @(
                'accountObjectId', 'accountSid', 'accountUpn', 'accountName', 'accountDomain',
                'accountId', 'requestAccountSid', 'requestAccountName', 'requestAccountDomain',
                'recipientObjectId', 'processAccountObjectId', 'initiatingAccountSid',
                'initiatingProcessAccountUpn', 'initiatingAccountName', 'initiatingAccountDomain',
                'servicePrincipalId', 'servicePrincipalName', 'targetAccountUpn',
                'initiatingProcessAccountObjectId', 'initiatingProcessAccountSid'
            )
            'Mailbox' = @(
                'accountUpn', 'fileOwnerUpn', 'initiatingProcessAccountUpn', 'lastModifyingAccountUpn',
                'targetAccountUpn', 'senderFromAddress', 'senderDisplayName', 'recipientEmailAddress',
                'senderMailFromAddress'
            )
        }

        $impactedAssets = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($entity in $YamlObject.impactedEntities) {
            # Map Machine to Device for Microsoft Graph API compliance
            $odataEntityType = if ($entity.entityType -eq 'Machine') { 'Device' } else { $entity.entityType }

            # Convert first letter to lowercase for Graph API compliance
            $identifier = if ($entity.entityIdentifier.Length -gt 0) {
                $entity.entityIdentifier.Substring(0, 1).ToLower() + $entity.entityIdentifier.Substring(1)
            } else {
                $entity.entityIdentifier
            }

            # Validate identifier for the entity type
            if ($validIdentifiers.ContainsKey($odataEntityType)) {
                if ($identifier -notin $validIdentifiers[$odataEntityType]) {
                    $validList = $validIdentifiers[$odataEntityType] -join ', '
                    if ($SkipIdentifierValidation) {
                        Write-Warning "Identifier '$identifier' for entity type '$odataEntityType' is not in the official documentation. Valid identifiers are: $validList"
                    } else {
                        throw "Invalid identifier '$identifier' for entity type '$odataEntityType'. Valid identifiers are: $validList"
                    }
                }
            }

            $impactedAssets.Add(@{
                    '@odata.type' = "#microsoft.graph.security.impacted$($odataEntityType)Asset"
                    identifier    = $identifier
                })
        }
        $jsonObj.detectionAction.alertTemplate.impactedAssets = $impactedAssets
    }

    # Map organizational scope
    if ($YamlObject.organizationalScope) {
        $jsonObj.detectionAction.organizationalScope = $YamlObject.organizationalScope
    }

    # Map response actions
    if ($YamlObject.actions) {
        $jsonObj.detectionAction.responseActions = $YamlObject.actions
    }

    return $jsonObj
}