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. #> [CmdletBinding()] param( [Parameter(Mandatory)] [PSObject]$JsonObject, [Parameter()] [bool]$SetEnabled, [Parameter()] [ValidateSet('Informational', 'Low', 'Medium', 'High')] [string]$SetSeverity, [Parameter()] [switch]$ValidateIdentifiers ) $DefaultSortOrderInYAML = @( 'guid' 'detectorId' 'ruleName' 'description' 'isEnabled' 'status' 'alertTitle' 'frequency' 'alertSeverity' 'alertDescription' 'alertRecommendedAction' 'tactics' 'entityMappings' 'customDetails' 'organizationalScope' '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 } $recommendedActions = Get-CustomDetectionValue -Object $JsonObject -Path 'detectionAction.alertTemplate.recommendedActions' if (Test-CustomDetectionValue $recommendedActions) { $yamlObj['alertRecommendedAction'] = $recommendedActions } $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 } |