Private/ConvertTo-CustomDetectionLegacyYaml.ps1
|
function ConvertTo-CustomDetectionLegacyYaml { <# .SYNOPSIS Rewrites a current-shape YAML object into the legacy YAML keys. .DESCRIPTION Produces alertCategory, mitreTechniques, impactedEntities, a legacy frequency token and actions without column mappings. Values the legacy keys cannot express are kept in their current form or dropped, each with a warning. #> [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param( [Parameter(Mandatory)] [PSObject]$YamlObject ) $yaml = ConvertTo-CustomDetectionHashtable -InputObject $YamlObject $sortOrder = @( 'guid', 'isEnabled', 'ruleName', 'alertTitle', 'alertCategory', 'alertDescription', 'frequency', 'alertSeverity', 'alertRecommendedAction', 'mitreTechniques', 'impactedEntities', 'organizationalScope', 'actions', 'queryText' ) $legacy = [ordered]@{} foreach ($key in @('guid', 'ruleName', 'isEnabled', 'alertTitle', 'alertSeverity', 'alertDescription', 'alertRecommendedAction', 'organizationalScope', 'queryText')) { if ($yaml.Contains($key)) { $legacy[$key] = $yaml[$key] } } if ($yaml.Contains('status')) { Write-Warning "Status '$($yaml['status'])' has no legacy key. Only isEnabled is emitted." } if ($yaml.Contains('detectorId')) { Write-Warning 'The detector id has no legacy key and is dropped. The guid identifies the rule.' } if ($yaml.Contains('description')) { Write-Warning 'The rule description has no legacy key and is dropped.' } if ($yaml.Contains('customDetails')) { Write-Warning 'customDetails has no legacy key and is dropped.' } $periodMap = @{ 'PT0S' = '0'; 'PT1H' = '1H'; 'PT3H' = '3H'; 'PT12H' = '12H'; 'P1D' = '24H'; 'PT24H' = '24H' } if ($yaml.Contains('frequency')) { $frequency = "$($yaml['frequency'])" $token = $periodMap[$frequency.ToUpperInvariant()] if ($token) { $legacy['frequency'] = $token } else { Write-Warning "Frequency '$frequency' has no legacy token and is kept as is." $legacy['frequency'] = $frequency } } $tactics = @($yaml['tactics']) if ($tactics.Count -gt 1) { Write-Warning "The rule lists $($tactics.Count) tactics. The legacy alertCategory keeps only the first." } if ($tactics.Count -gt 0) { $first = ConvertTo-CustomDetectionHashtable -InputObject $tactics[0] $legacy['alertCategory'] = "$($first['tactic'])" $techniques = [System.Collections.Generic.List[string]]::new() foreach ($technique in @($first['techniques'])) { if ($technique -is [string]) { if (-not $techniques.Contains($technique)) { $techniques.Add($technique) } continue } $map = ConvertTo-CustomDetectionHashtable -InputObject $technique if (-not $map) { continue } if ($map['technique'] -and -not $techniques.Contains("$($map['technique'])")) { $techniques.Add("$($map['technique'])") } foreach ($sub in @($map['subTechniques'])) { if ($sub -and -not $techniques.Contains("$sub")) { $techniques.Add("$sub") } } } if ($techniques.Count -gt 0) { $legacy['mitreTechniques'] = [object[]]@($techniques | Sort-Object) } } $entityTypes = @{ hosts = 'Machine'; accounts = 'User'; mailboxes = 'Mailbox' } $columnTypes = @{ ips = @{ addressColumn = 'IP' } urls = @{ addressColumn = 'URL' } files = @{ sha1Column = 'FileHash'; sha256Column = 'FileHash' } processes = @{ sha1Column = 'Process'; sha256Column = 'Process' } registryValues = @{ keyColumn = 'RegistryKey'; valueNameColumn = 'RegistryValue' } } $legacyIdentifiers = Get-CustomDetectionLegacyIdentifierMap $impactedEntities = [System.Collections.Generic.List[object]]::new() $entityMappings = ConvertTo-CustomDetectionHashtable -InputObject $yaml['entityMappings'] if ($entityMappings) { foreach ($collection in $entityMappings.Keys) { if ($columnTypes.ContainsKey($collection)) { foreach ($item in @($entityMappings[$collection])) { $columns = ConvertTo-CustomDetectionHashtable -InputObject $item if (-not $columns) { continue } foreach ($column in $columns.Keys) { $value = "$($columns[$column])" if (-not $value) { continue } $legacyType = $columnTypes[$collection][$column] if (-not $legacyType) { Write-Warning "Entity mapping $collection.$column = '$value' has no legacy entity type and is dropped." continue } $impactedEntities.Add([ordered]@{ entityType = $legacyType entityIdentifier = $value.Substring(0, 1).ToLowerInvariant() + $value.Substring(1) }) } } continue } if (-not $entityTypes.ContainsKey($collection)) { Write-Warning "Entity mapping '$collection' has no legacy entity type and is dropped." continue } foreach ($item in @($entityMappings[$collection])) { $columns = ConvertTo-CustomDetectionHashtable -InputObject $item if (-not $columns) { continue } foreach ($column in $columns.Keys) { $value = "$($columns[$column])" if (-not $value) { continue } $identifier = $value.Substring(0, 1).ToLowerInvariant() + $value.Substring(1) $known = @($legacyIdentifiers[$collection][$column]) | Where-Object { $_ -eq $identifier } | Select-Object -First 1 if (-not $known) { Write-Warning "Entity mapping $collection.$column = '$value' has no legacy identifier and is dropped." continue } $impactedEntities.Add([ordered]@{ entityType = $entityTypes[$collection] entityIdentifier = $known }) } } } } if ($impactedEntities.Count -gt 0) { $legacy['impactedEntities'] = [object[]]$impactedEntities.ToArray() } $legacyActionTypes = @('IsolateMachine', 'CollectInvestigationPackage', 'RunAntivirusScan', 'InitiateInvestigation', 'RestrictAppExecution') $actionMap = Get-CustomDetectionActionMap $actions = [System.Collections.Generic.List[object]]::new() foreach ($item in @($yaml['actions'])) { $map = ConvertTo-CustomDetectionHashtable -InputObject $item if (-not $map) { continue } $actionType = "$($map['actionType'])" $known = $legacyActionTypes | Where-Object { $_ -eq $actionType } | Select-Object -First 1 if (-not $known) { Write-Warning "Action '$actionType' has no legacy action type and is dropped." continue } $action = [ordered]@{ actionType = $known } $fields = ConvertTo-CustomDetectionHashtable -InputObject $map['additionalFields'] if ($known -eq 'IsolateMachine' -and $fields -and $fields['isolationType']) { $action['additionalFields'] = [ordered]@{ isolationType = (Get-Culture).TextInfo.ToTitleCase("$($fields['isolationType'])".ToLowerInvariant()) } } # The legacy keys carry no column mappings, so anything beyond the defaults is lost if ($fields) { $defaults = ($actionMap | Where-Object { $_.ActionType -eq $known } | Select-Object -First 1).Defaults foreach ($field in @($fields.Keys)) { if ($field -eq 'isolationType') { continue } if ($defaults.Contains($field) -and "$($defaults[$field])" -eq "$($fields[$field])") { continue } Write-Warning "Action '$known' column mapping $field = '$($fields[$field])' has no legacy form and is dropped. The rule redeploys with the default." } } $actions.Add($action) } if ($actions.Count -gt 0) { $legacy['actions'] = [object[]]$actions.ToArray() } return ConvertTo-CustomDetectionOrderedMap -Map $legacy -Order $sortOrder } |