Public/Test-CustomDetectionMitreTechnique.ps1

function Test-CustomDetectionMitreTechnique {
    <#
    .SYNOPSIS
        Validates that MITRE ATT&CK techniques in a detection rule are supported by XDR for the selected category.
 
    .DESCRIPTION
        Reads the tactics list, or the legacy alertCategory and mitreTechniques, from a detection
        YAML file or object and checks each technique against the set of techniques that Microsoft
        XDR supports for that category. Techniques not supported by XDR for the given category are
        reported as invalid. With a tactics list every tactic is validated and the results are combined.
 
        The technique mapping is derived from the XDR portal's front-end data and reflects the
        actual techniques available in each alert category dropdown.
 
        Categories not present in the XDR mapping (e.g. SuspiciousActivity) cannot be validated;
        in that case the function emits a warning and returns IsValid = $true with no invalid techniques.
 
    .PARAMETER InputFile
        Path to a detection YAML (.yaml/.yml) file.
 
    .PARAMETER InputObject
        A parsed YAML/detection PSObject. Accepts pipeline input.
 
    .OUTPUTS
        PSCustomObject with:
          IsValid - $true if all listed techniques are supported for their category
          Category - the tactic name(s), comma separated when several tactics are listed
          ValidTechniques - techniques that are supported for the category
          InvalidTechniques- techniques that are NOT supported for the category
 
    .EXAMPLE
        Test-CustomDetectionMitreTechnique -InputFile '.\detection.yaml'
 
        Validates all MITRE techniques in the YAML file against the XDR category mapping.
 
    .EXAMPLE
        Get-ChildItem '.\detections\*.yaml' | ForEach-Object { Test-CustomDetectionMitreTechnique -InputFile $_.FullName }
 
        Validates all YAML files in a directory.
 
    .EXAMPLE
        ConvertFrom-Yaml (Get-Content '.\detection.yaml' -Raw) | Test-CustomDetectionMitreTechnique
 
        Pipeline input from a parsed YAML object.
 
    .EXAMPLE
        $result = Test-CustomDetectionMitreTechnique -InputFile '.\detection.yaml'
        if (-not $result.IsValid) {
            Write-Warning "Unsupported techniques: $($result.InvalidTechniques -join ', ')"
        }
    #>

    [CmdletBinding(DefaultParameterSetName = 'File')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, ParameterSetName = 'File', HelpMessage = 'Path to a detection YAML file')]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]$InputFile,

        [Parameter(Mandatory, ParameterSetName = 'Object', ValueFromPipeline, HelpMessage = 'Parsed detection object')]
        [ValidateNotNull()]
        [PSObject]$InputObject
    )

    begin {
        # XDR MITRE technique mapping per alert category
        # Source: Microsoft XDR portal front-end (xdr-main.js)
        $xdrTechniqueMap = @{
            DefenseEvasion          = @('T1055.011', 'T1205.002', 'T1066', 'T1027.011', 'T1218.011', 'T1143', 'T1027.009', 'T1150', 'T1556.003', 'T1578.004', 'T1148', 'T1564.012', 'T1222.002', 'T1216.001', 'T1574.007', 'T1006', 'T1666', 'T1564.008', 'T1027.013', 'T1014', 'T1109', 'T1036.007', 'T1548.002', 'T1099', 'T1548.003', 'T1578', 'T1542.001', 'T1574.011', 'T1542.003', 'T1116', 'T1218.013', 'T1093', 'T1036.005', 'T1600', 'T1036.008', 'T1121', 'T1564', 'T1484.002', 'T1527', 'T1562.009', 'T1542.005', 'T1497.001', 'T1070.002', 'T1218.004', 'T1089', 'T1027.008', 'T1574.001', 'T1553.001', 'T1553.002', 'T1036.009', 'T1222.001', 'T1574.014', 'T1218.007', 'T1556.002', 'T1070.007', 'T1600.001', 'T1070.003', 'T1202', 'T1536', 'T1140', 'T1562', 'T1055.003', 'T1036', 'T1070.008', 'T1055', 'T1205', 'T1218', 'T1038', 'T1070.006', 'T1620', 'T1480.002', 'T1564.011', 'T1497.003', 'T1218.003', 'T1562.002', 'T1218.002', 'T1599.001', 'T1036.011', 'T1009', 'T1550', 'T1181', 'T1562.004', 'T1152', 'T1553.003', 'T1556.007', 'T1218.015', 'T1562.012', 'T1207', 'T1553.006', 'T1610', 'T1107', 'T1112', 'T1574.008', 'T1535', 'T1564.013', 'T1027.001', 'T1484.001', 'T1078.001', 'T1183', 'T1085', 'T1574.006', 'T1070.001', 'T1222', 'T1027.016', 'T1548', 'T1134.002', 'T1548.001', 'T1117', 'T1054', 'T1108', 'T1218.008', 'T1548.005', 'T1144', 'T1045', 'T1055.013', 'T1578.003', 'T1574.005', 'T1198', 'T1562.006', 'T1564.014', 'T1562.007', 'T1036.002', 'T1027.017', 'T1542.002', 'T1070', 'T1550.003', 'T1036.004', 'T1055.004', 'T1647', 'T1127.003', 'T1191', 'T1553.005', 'T1600.002', 'T1542', 'T1064', 'T1612', 'T1055.002', 'T1218.012', 'T1562.010', 'T1497', 'T1218.005', 'T1480', 'T1134.001', 'T1205.001', 'T1027.012', 'T1564.002', 'T1134.003', 'T1196', 'T1562.003', 'T1556.008', 'T1497.002', 'T1134.004', 'T1055.014', 'T1122', 'T1502', 'T1574.010', 'T1149', 'T1170', 'T1574.013', 'T1542.004', 'T1218.001', 'T1070.005', 'T1562.001', 'T1601', 'T1574', 'T1027.005', 'T1078', 'T1073', 'T1055.012', 'T1564.009', 'T1027', 'T1556.006', 'T1036.001', 'T1564.006', 'T1027.014', 'T1134.005', 'T1599', 'T1553', 'T1548.004', 'T1218.010', 'T1036.003', 'T1562.011', 'T1574.009', 'T1186', 'T1027.003', 'T1550.004', 'T1078.002', 'T1218.009', 'T1506', 'T1553.004', 'T1027.004', 'T1564.007', 'T1197', 'T1127.001', 'T1656', 'T1578.005', 'T1088', 'T1562.008', 'T1564.003', 'T1127.002', 'T1070.010', 'T1147', 'T1556.009', 'T1578.002', 'T1500', 'T1055.009', 'T1223', 'T1601.001', 'T1070.009', 'T1146', 'T1036.010', 'T1556.001', 'T1027.006', 'T1556.005', 'T1027.010', 'T1130', 'T1070.004', 'T1158', 'T1221', 'T1134', 'T1027.002', 'T1564.005', 'T1672', 'T1151', 'T1055.005', 'T1622', 'T1036.006', 'T1550.002', 'T1574.002', 'T1216.002', 'T1126', 'T1548.006', 'T1055.008', 'T1027.007', 'T1055.015', 'T1484', 'T1220', 'T1564.001', 'T1578.001', 'T1550.001', 'T1078.004', 'T1480.001', 'T1564.004', 'T1096', 'T1055.001', 'T1556', 'T1216', 'T1118', 'T1556.004', 'T1027.015', 'T1574.004', 'T1601.002', 'T1078.003', 'T1211', 'T1127', 'T1218.014', 'T1564.010', 'T1574.012')
            PrivilegeEscalation     = @('T1055.011', 'T1053.005', 'T1037', 'T1150', 'T1574.007', 'T1044', 'T1546.013', 'T1514', 'T1543', 'T1546.006', 'T1053.007', 'T1548.002', 'T1548.003', 'T1574.011', 'T1178', 'T1547', 'T1013', 'T1206', 'T1547.014', 'T1484.002', 'T1543.003', 'T1053.003', 'T1165', 'T1098.003', 'T1547.012', 'T1574.001', 'T1103', 'T1574.014', 'T1098.006', 'T1053', 'T1058', 'T1098.007', 'T1055.003', 'T1546.011', 'T1547.010', 'T1037.002', 'T1055', 'T1038', 'T1050', 'T1611', 'T1547.009', 'T1182', 'T1547.005', 'T1181', 'T1543.004', 'T1574.008', 'T1484.001', 'T1078.001', 'T1547.003', 'T1183', 'T1546.005', 'T1574.006', 'T1053.001', 'T1179', 'T1547.011', 'T1548', 'T1134.002', 'T1548.001', 'T1547.004', 'T1098.004', 'T1546.012', 'T1548.005', 'T1055.013', 'T1574.005', 'T1546.008', 'T1504', 'T1055.004', 'T1138', 'T1546.009', 'T1098.005', 'T1055.002', 'T1547.015', 'T1134.001', 'T1098.001', 'T1134.003', 'T1053.004', 'T1546.003', 'T1134.004', 'T1546.001', 'T1055.014', 'T1015', 'T1546.014', 'T1502', 'T1169', 'T1574.010', 'T1547.001', 'T1098', 'T1547.006', 'T1574.013', 'T1053.006', 'T1157', 'T1574', 'T1543.005', 'T1078', 'T1055.012', 'T1068', 'T1546', 'T1546.004', 'T1134.005', 'T1548.004', 'T1547.002', 'T1546.015', 'T1574.009', 'T1166', 'T1037.005', 'T1100', 'T1078.002', 'T1034', 'T1037.003', 'T1088', 'T1546.010', 'T1546.002', 'T1543.001', 'T1055.009', 'T1519', 'T1546.016', 'T1037.004', 'T1134', 'T1543.002', 'T1547.013', 'T1055.005', 'T1547.007', 'T1574.002', 'T1098.002', 'T1548.006', 'T1160', 'T1055.008', 'T1037.001', 'T1055.015', 'T1484', 'T1547.008', 'T1078.004', 'T1053.002', 'T1055.001', 'T1546.017', 'T1546.007', 'T1574.004', 'T1078.003', 'T1574.012', 'T0874', 'T0890')
            Execution               = @('T1053.005', 'T1047', 'T1129', 'T1059.007', 'T1053.007', 'T1121', 'T1559.002', 'T1204.002', 'T1053.003', 'T1559.001', 'T1675', 'T1053', 'T1059.002', 'T1106', 'T1059.010', 'T1153', 'T1569.003', 'T1152', 'T1059.009', 'T1610', 'T1155', 'T1085', 'T1674', 'T1053.001', 'T1117', 'T1177', 'T1059', 'T1175', 'T1609', 'T1191', 'T1064', 'T1569.001', 'T1059.008', 'T1559.003', 'T1204', 'T1196', 'T1053.004', 'T1072', 'T1059.001', 'T1170', 'T1053.006', 'T1061', 'T1059.004', 'T1559', 'T1059.011', 'T1204.003', 'T1154', 'T1203', 'T1168', 'T1028', 'T1059.006', 'T1569', 'T1059.003', 'T1223', 'T1059.012', 'T1651', 'T1059.005', 'T1204.004', 'T1151', 'T1648', 'T1173', 'T1204.001', 'T1569.002', 'T1053.002', 'T1035', 'T1086', 'T1118', 'T0821', 'T0807', 'T0863', 'T0858', 'T0853', 'T0871', 'T0895', 'T0875', 'T0874', 'T0844', 'T0823', 'T0834')
            Persistence             = @('T1053.005', 'T1205.002', 'T1156', 'T1067', 'T1037', 'T1161', 'T1150', 'T1556.003', 'T1574.007', 'T1044', 'T1546.013', 'T1501', 'T1543', 'T1133', 'T1109', 'T1546.006', 'T1053.007', 'T1542.001', 'T1574.011', 'T1163', 'T1542.003', 'T1547', 'T1013', 'T1547.014', 'T1176.001', 'T1180', 'T1542.005', 'T1543.003', 'T1053.003', 'T1165', 'T1137', 'T1098.003', 'T1547.012', 'T1574.001', 'T1103', 'T1137.006', 'T1505.002', 'T1574.014', 'T1098.006', 'T1053', 'T1162', 'T1556.002', 'T1505.005', 'T1176', 'T1058', 'T1137.005', 'T1098.007', 'T1546.011', 'T1547.010', 'T1037.002', 'T1205', 'T1038', 'T1050', 'T1547.009', 'T1062', 'T1182', 'T1525', 'T1547.005', 'T1004', 'T1131', 'T1152', 'T1556.007', 'T1112', 'T1543.004', 'T1574.008', 'T1505.003', 'T1078.001', 'T1547.003', 'T1183', 'T1031', 'T1546.005', 'T1574.006', 'T1136.001', 'T1053.001', 'T1176.002', 'T1179', 'T1547.011', 'T1547.004', 'T1019', 'T1042', 'T1164', 'T1108', 'T1098.004', 'T1215', 'T1101', 'T1546.012', 'T1177', 'T1574.005', 'T1546.008', 'T1504', 'T1198', 'T1136.002', 'T1542.002', 'T1137.001', 'T1138', 'T1546.009', 'T1098.005', 'T1542', 'T1547.015', 'T1205.001', 'T1098.001', 'T1053.004', 'T1556.008', 'T1546.003', 'T1060', 'T1554', 'T1023', 'T1546.001', 'T1122', 'T1015', 'T1546.014', 'T1574.010', 'T1547.001', 'T1136.003', 'T1098', 'T1547.006', 'T1574.013', 'T1053.006', 'T1542.004', 'T1137.003', 'T1157', 'T1574', 'T1543.005', 'T1078', 'T1556.006', 'T1505.004', 'T1154', 'T1546', 'T1546.004', 'T1547.002', 'T1128', 'T1546.015', 'T1137.004', 'T1574.009', 'T1168', 'T1166', 'T1037.005', 'T1100', 'T1671', 'T1078.002', 'T1034', 'T1037.003', 'T1197', 'T1546.010', 'T1546.002', 'T1556.009', 'T1543.001', 'T1519', 'T1505', 'T1556.001', 'T1556.005', 'T1546.016', 'T1158', 'T1037.004', 'T1209', 'T1159', 'T1543.002', 'T1668', 'T1136', 'T1547.013', 'T1547.007', 'T1574.002', 'T1098.002', 'T1084', 'T1160', 'T1653', 'T1037.001', 'T1137.002', 'T1547.008', 'T1078.004', 'T1053.002', 'T1556', 'T1546.017', 'T1546.007', 'T1505.006', 'T1505.001', 'T1556.004', 'T1574.004', 'T1078.003', 'T1574.012', 'T0857', 'T0891', 'T0859', 'T0873', 'T0839', 'T0889')
            CommandAndControl       = @('T1205.002', 'T1132.001', 'T1568.002', 'T1071.004', 'T1172', 'T1071.005', 'T1573.001', 'T1568.001', 'T1071', 'T1024', 'T1219', 'T1079', 'T1659', 'T1205', 'T1032', 'T1572', 'T1483', 'T1071.003', 'T1092', 'T1090.002', 'T1090', 'T1219.001', 'T1568', 'T1188', 'T1102', 'T1568.003', 'T1104', 'T1205.001', 'T1026', 'T1071.002', 'T1102.003', 'T1090.003', 'T1219.003', 'T1001', 'T1571', 'T1573', 'T1102.002', 'T1573.002', 'T1095', 'T1001.003', 'T1065', 'T1090.004', 'T1132', 'T1219.002', 'T1132.002', 'T1071.001', 'T1105', 'T1665', 'T1001.002', 'T1008', 'T1090.001', 'T1094', 'T1102.001', 'T1001.001', 'T1043', 'T0884', 'T0869', 'T0885')
            Collection              = @('T1560.001', 'T1113', 'T1557', 'T1056.001', 'T1602', 'T1213.002', 'T1123', 'T1560.003', 'T1114', 'T1025', 'T1074.001', 'T1114.001', 'T1119', 'T1115', 'T1530', 'T1074.002', 'T1005', 'T1560.002', 'T1557.004', 'T1602.002', 'T1560', 'T1185', 'T1557.003', 'T1557.001', 'T1056.003', 'T1125', 'T1213.001', 'T1114.003', 'T1074', 'T1056.002', 'T1039', 'T1114.002', 'T1056', 'T1213.004', 'T1557.002', 'T1213.003', 'T1213', 'T1602.001', 'T1056.004', 'T1213.005', 'T0887', 'T0850', 'T0861', 'T0868', 'T0801', 'T0845', 'T0811', 'T0802', 'T0877', 'T0825', 'T0870', 'T0830', 'T0852', 'T0893')
            LateralMovement         = @('T1021.005', 'T1080', 'T1527', 'T1021.004', 'T1017', 'T1091', 'T1021.008', 'T1563.001', 'T1021.002', 'T1550', 'T1076', 'T1021', 'T1563', 'T1021.006', 'T1021.003', 'T1175', 'T1550.003', 'T1051', 'T1021.007', 'T1072', 'T1210', 'T1534', 'T1097', 'T1570', 'T1184', 'T1075', 'T1028', 'T1550.004', 'T1506', 'T1563.002', 'T1550.002', 'T1021.001', 'T1550.001', 'T1077', 'T0866', 'T0812', 'T0844', 'T0843', 'T0891', 'T0859', 'T0886', 'T0867')
            CredentialAccess        = @('T1557', 'T1556.003', 'T1056.001', 'T1110.001', 'T1003', 'T1171', 'T1539', 'T1003.002', 'T1552.005', 'T1555.002', 'T1522', 'T1110.002', 'T1555.001', 'T1003.004', 'T1606.002', 'T1167', 'T1214', 'T1003.007', 'T1555.005', 'T1040', 'T1552.002', 'T1556.002', 'T1558.005', 'T1558.004', 'T1558', 'T1555', 'T1552', 'T1139', 'T1503', 'T1557.004', 'T1556.007', 'T1145', 'T1555.003', 'T1557.003', 'T1552.004', 'T1557.001', 'T1003.001', 'T1179', 'T1110.003', 'T1056.003', 'T1003.005', 'T1558.001', 'T1649', 'T1552.003', 'T1552.001', 'T1606.001', 'T1528', 'T1552.006', 'T1556.008', 'T1141', 'T1606', 'T1621', 'T1552.008', 'T1212', 'T1142', 'T1056.002', 'T1110', 'T1110.004', 'T1208', 'T1556.006', 'T1187', 'T1174', 'T1081', 'T1056', 'T1557.002', 'T1556.009', 'T1555.006', 'T1003.008', 'T1558.002', 'T1555.004', 'T1556.001', 'T1556.005', 'T1111', 'T1003.003', 'T1558.003', 'T1003.006', 'T1556', 'T1056.004', 'T1552.007', 'T1556.004')
            Discovery               = @('T1033', 'T1613', 'T1016.001', 'T1069', 'T1069.003', 'T1615', 'T1652', 'T1087.002', 'T1063', 'T1087.001', 'T1497.001', 'T1069.002', 'T1007', 'T1040', 'T1135', 'T1120', 'T1082', 'T1016.002', 'T1010', 'T1087.003', 'T1497.003', 'T1580', 'T1217', 'T1673', 'T1016', 'T1087', 'T1482', 'T1083', 'T1049', 'T1497', 'T1619', 'T1654', 'T1087.004', 'T1057', 'T1497.002', 'T1069.001', 'T1201', 'T1614.001', 'T1012', 'T1614', 'T1518.001', 'T1526', 'T1018', 'T1046', 'T1518', 'T1538', 'T1622', 'T1124', 'T0887', 'T0888', 'T0842', 'T0841', 'T0854', 'T0808', 'T0846', 'T0824', 'T0840')
            ResourceDevelopment     = @('T1583', 'T1583.007', 'T1588.007', 'T1584.008', 'T1583.008', 'T1588.004', 'T1583.002', 'T1587.003', 'T1587.001', 'T1586.001', 'T1588.006', 'T1583.005', 'T1608.004', 'T1587.002', 'T1584.003', 'T1586.003', 'T1586.002', 'T1608.001', 'T1583.001', 'T1608.002', 'T1583.004', 'T1585.002', 'T1588.001', 'T1583.003', 'T1584', 'T1586', 'T1584.005', 'T1608', 'T1608.005', 'T1583.006', 'T1585.003', 'T1588.002', 'T1584.006', 'T1585.001', 'T1587.004', 'T1608.003', 'T1584.002', 'T1585', 'T1588', 'T1650', 'T1584.007', 'T1584.004', 'T1608.006', 'T1588.003', 'T1587', 'T1588.005', 'T1584.001')
            Reconnaissance          = @('T1592', 'T1596.003', 'T1597.002', 'T1590.005', 'T1590.002', 'T1596.002', 'T1594', 'T1596.001', 'T1591.003', 'T1592.001', 'T1598.003', 'T1590.004', 'T1590.003', 'T1597.001', 'T1589', 'T1595.002', 'T1596', 'T1595', 'T1589.002', 'T1598.004', 'T1590.006', 'T1593.002', 'T1591.002', 'T1593.003', 'T1589.003', 'T1592.004', 'T1598.002', 'T1596.004', 'T1591', 'T1590', 'T1593', 'T1597', 'T1592.003', 'T1592.002', 'T1593.001', 'T1589.001', 'T1595.003', 'T1591.004', 'T1598', 'T1595.001', 'T1590.001', 'T1596.005', 'T1591.001', 'T1598.001')
            Impact                  = @('T1561.002', 'T1498.001', 'T1492', 'T1491.002', 'T1499.001', 'T1485.001', 'T1496.003', 'T1499.003', 'T1561', 'T1565.001', 'T1489', 'T1499.004', 'T1487', 'T1565.003', 'T1498.002', 'T1499.002', 'T1491', 'T1496.002', 'T1657', 'T1491.001', 'T1496.004', 'T1496.001', 'T1565', 'T1531', 'T1486', 'T1488', 'T1667', 'T1499', 'T1494', 'T1493', 'T1496', 'T1565.002', 'T1485', 'T1498', 'T1495', 'T1490', 'T1561.001', 'T1529', 'T0829', 'T0831', 'T0837', 'T0832', 'T0815', 'T0880', 'T0828', 'T0879', 'T0827', 'T0826', 'T0882', 'T0813')
            InitialAccess           = @('T1133', 'T1195.001', 'T1192', 'T1566.002', 'T1566.001', 'T1195.003', 'T1091', 'T1195', 'T1190', 'T1659', 'T1078.001', 'T1193', 'T1199', 'T1566', 'T1078', 'T1566.004', 'T1195.002', 'T1078.002', 'T1194', 'T1200', 'T1189', 'T1078.004', 'T1566.003', 'T1078.003', 'T1669', 'T0860', 'T0819', 'T0864', 'T0810', 'T0862', 'T0865', 'T0817', 'T0866', 'T0822', 'T0848', 'T0847', 'T0818', 'T0886', 'T0883')
            Exfiltration            = @('T1567', 'T1567.004', 'T1029', 'T1011', 'T1011.001', 'T1020', 'T1048.001', 'T1020.001', 'T1567.001', 'T1048.002', 'T1041', 'T1048', 'T1052.001', 'T1002', 'T1567.003', 'T1567.002', 'T1030', 'T1537', 'T1022', 'T1052', 'T1048.003')
            InhibitResponseFunction = @('T0803', 'T0881', 'T0800', 'T0814', 'T0805', 'T0816', 'T0878', 'T0835', 'T0851', 'T0804', 'T0809', 'T0857', 'T0833', 'T0838', 'T0892')
            ImpairProcessControl    = @('T0836', 'T0855', 'T0856', 'T0806', 'T0875', 'T0833', 'T0839')
            Evasion                 = @('T0894', 'T0858', 'T0851', 'T0872', 'T0856', 'T0820', 'T0849')
        }
    }

    process {
        try {
            $yamlObj = if ($PSCmdlet.ParameterSetName -eq 'File') {
                Import-CustomDetectionYamlFile -FilePath $InputFile
            } else {
                $InputObject
            }

            # Build one (category, techniques) pair per tactic. A tactics list wins over the legacy keys.
            $sets = [System.Collections.Generic.List[object]]::new()
            if ($yamlObj.tactics) {
                # Entries are validated as written. A listed parent counts alongside its sub-techniques, a derived one does not
                foreach ($tacticItem in @($yamlObj.tactics)) {
                    $tactic = ConvertTo-CustomDetectionHashtable -InputObject $tacticItem
                    if (-not $tactic) { continue }
                    $flattened = [System.Collections.Generic.List[string]]::new()
                    foreach ($technique in @($tactic['techniques'])) {
                        if ($null -eq $technique) { continue }
                        if ($technique -is [string]) {
                            if ($technique.Trim()) { $flattened.Add($technique.Trim()) }
                            continue
                        }
                        $entry = ConvertTo-CustomDetectionHashtable -InputObject $technique
                        if (-not $entry) { continue }
                        if ("$($entry['technique'])".Trim()) { $flattened.Add("$($entry['technique'])".Trim()) }
                        foreach ($sub in @($entry['subTechniques'])) {
                            if ("$sub".Trim()) { $flattened.Add("$sub".Trim()) }
                        }
                    }
                    $sets.Add(@{ Category = "$($tactic['tactic'])"; Techniques = $flattened.ToArray() })
                }
            } else {
                $category = $yamlObj.alertCategory
                if (-not $category) {
                    throw "The detection object does not contain an 'alertCategory' or 'tactics' property."
                }
                $sets.Add(@{ Category = $category; Techniques = @($yamlObj.mitreTechniques | Where-Object { $_ }) })
            }

            $validTechniques = [System.Collections.Generic.List[string]]::new()
            $invalidTechniques = [System.Collections.Generic.List[string]]::new()

            foreach ($set in $sets) {
                $category = $set.Category
                $techniques = @($set.Techniques)

                # No techniques defined — nothing to validate
                if ($techniques.Count -eq 0) {
                    Write-Verbose "No techniques defined for category '$category'. Nothing to validate."
                    continue
                }

                # Category not present in XDR mapping — cannot validate
                if (-not $xdrTechniqueMap.ContainsKey($category)) {
                    Write-Warning "Category '$category' is not present in the XDR technique mapping. Validation skipped."
                    foreach ($technique in $techniques) { $validTechniques.Add($technique) }
                    continue
                }

                $supported = $xdrTechniqueMap[$category]
                $invalidForCategory = [System.Collections.Generic.List[string]]::new()
                foreach ($technique in $techniques) {
                    if ($technique -in $supported) {
                        $validTechniques.Add($technique)
                    } else {
                        $invalidTechniques.Add($technique)
                        $invalidForCategory.Add($technique)
                    }
                }

                if ($invalidForCategory.Count -gt 0) {
                    $invalidList = $invalidForCategory -join ', '
                    Write-Warning "The following MITRE technique(s) are not supported by XDR for category '$category': $invalidList"
                }
            }

            [PSCustomObject]@{
                IsValid           = ($invalidTechniques.Count -eq 0)
                Category          = (($sets | ForEach-Object { $_.Category }) -join ', ')
                ValidTechniques   = $validTechniques.ToArray()
                InvalidTechniques = $invalidTechniques.ToArray()
            }
        } catch {
            Write-Error "Error validating MITRE techniques: $_"
            throw
        }
    }
}