Private/Parse-CISYAML.ps1

<#
.SYNOPSIS
    Parses CIS YAML files and extracts registry/security policy settings.
.DESCRIPTION
    Reads CIS benchmark YAML files and extracts all registry settings, security policies, and user rights assignments.
#>

function Parse-CISYAML {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$YAMLPath,
        
        [Parameter(Mandatory = $false)]
        [ValidateSet("Level1", "Level2", "All")]
        [string]$Level = "All"
    )

    $results = @()
    
    if (-not (Test-Path $YAMLPath)) {
        Write-Warning "YAML file not found: $YAMLPath"
        return $results
    }

    $content = Get-Content $YAMLPath -Raw
    
    # Extract control ID from name (e.g., "1.1.1 | PATCH | ...")
    $controlIdMatch = [regex]::Match($content, '(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)\s*\|\s*PATCH')
    $controlId = if ($controlIdMatch.Success) { $controlIdMatch.Groups[1].Value } else { "Unknown" }
    
    # Extract level tags
    $hasLevel1 = $content -match 'level1-corporate-enterprise-environment'
    $hasLevel2 = $content -match 'level2-high-security-sensitive-data-environment'
    
    # Filter by level
    if ($Level -eq "Level1" -and -not $hasLevel1) { return $results }
    if ($Level -eq "Level2" -and -not $hasLevel2) { return $results }
    
    # Extract registry settings (win_regedit) - improved line-by-line parsing
    $lines = $content -split "`r?`n"
    for ($i = 0; $i -lt $lines.Count; $i++) {
        if ($lines[$i] -match 'ansible\.windows\.win_regedit:') {
            # Found regedit block, extract next 4 lines (path, name, data, type)
            $registryPath = $null
            $registryName = $null
            $registryValue = $null
            $registryType = $null
            
            # Look ahead for path, name, data, type (within next 15 lines)
            for ($j = $i + 1; $j -lt [Math]::Min($i + 15, $lines.Count); $j++) {
                $testLine = $lines[$j]
                
                # Check if we've left the regedit block
                if ($testLine -match '^\s{0,2}(when|tags|loop|notify):' -or $testLine -match '^\s*-\s+name:') {
                    break # End of regedit block
                }
                
                # Match path - extract everything after "path:"
                if ($testLine.Contains('path:')) {
                    $pathIndex = $testLine.IndexOf('path:')
                    if ($pathIndex -ge 0) {
                        $pathValue = $testLine.Substring($pathIndex + 5).Trim()
                        # Direct assignment - pathValue already contains the full path
                        $registryPath = $pathValue
                    }
                }
                # Match name - extract everything after "name:" (but not task names)
                if ($testLine.Contains('name:') -and -not $testLine.Contains('name: "') -and -not $testLine.Contains('- name:')) {
                    $nameIndex = $testLine.IndexOf('name:')
                    if ($nameIndex -ge 0) {
                        $nameValue = $testLine.Substring($nameIndex + 5).Trim()
                        $registryName = $nameValue
                    }
                }
                # Match data - extract everything after "data:"
                if ($testLine.Contains('data:')) {
                    $dataIndex = $testLine.IndexOf('data:')
                    if ($dataIndex -ge 0) {
                        $dataValue = $testLine.Substring($dataIndex + 5).Trim()
                        $registryValue = $dataValue -replace '^["'']|["'']$', ''
                    }
                }
                # Match type - extract everything after "type:"
                if ($testLine.Contains('type:')) {
                    $typeIndex = $testLine.IndexOf('type:')
                    if ($typeIndex -ge 0) {
                        $typeValue = $testLine.Substring($typeIndex + 5).Trim()
                        $registryType = $typeValue
                        # We have all we need, process and break
                        if ($registryPath -and $registryName -and $null -ne $registryValue -and $registryType) {
                            # Handle HKU paths
                            if ($registryPath -match '^HKU:\\\{\{\s*item\s*\}\}\\(.+)') {
                                $userPath = $Matches[1]
                                $registryPath = "HKCU:\$userPath"
                            }
                            elseif ($registryPath -match '^HKU:\\') {
                                # Skip HKU without item variable
                                break
                            }
                            
                            # Handle variable references
                            if ($registryValue -match '\{\{\s*([^}]+)\s*\}\}') {
                                $varName = $Matches[1].Trim()
                                $registryValue = Get-CISDefaultValue -VariableName $varName
                            }
                            
                            $omaUri = Convert-RegistryPathToOMAURI -RegistryPath $registryPath -RegistryName $registryName
                            
                            $results += @{
                                ControlId = $controlId
                                Type = "Registry"
                                RegistryPath = $registryPath
                                RegistryName = $registryName
                                Value = $registryValue
                                DataType = $registryType
                                OMAUri = $omaUri
                                Level1 = $hasLevel1
                                Level2 = $hasLevel2
                                SourceFile = $YAMLPath
                            }
                        }
                        break # We have all we need
                    }
                }
            }
        }
    }
    
    # Extract security policy settings (win_security_policy) - line-by-line approach
    $inSecurityPolicyBlock = $false
    $currentSection = $null
    $currentKey = $null
    $currentValue = $null
    
    for ($i = 0; $i -lt $lines.Count; $i++) {
        $line = $lines[$i]
        
        if ($line -match 'community\.windows\.win_security_policy:') {
            $inSecurityPolicyBlock = $true
            $currentSection = $null
            $currentKey = $null
            $currentValue = $null
            continue
        }
        
        if ($inSecurityPolicyBlock) {
            if ($line -match '^\s*-\s+name:') {
                if ($currentSection -and $currentKey -and $currentValue) {
                    if ($currentValue -match '\{\{\s*([^}]+)\s*\}\}') {
                        $varName = $Matches[1].Trim()
                        $currentValue = Get-CISDefaultValue -VariableName $varName
                    }
                    
                    $omaUri = Convert-SecurityPolicyToOMAURI -Section $currentSection -Key $currentKey
                    
                    $results += @{
                        ControlId = $controlId
                        Type = "SecurityPolicy"
                        Section = $currentSection
                        Key = $currentKey
                        Value = $currentValue
                        OMAUri = $omaUri
                        Level1 = $hasLevel1
                        Level2 = $hasLevel2
                        SourceFile = $YAMLPath
                    }
                }
                $inSecurityPolicyBlock = $false
                continue
            }
            
            if ($line -match '^\s+section:\s*(.+)') {
                $currentSection = $Matches[1].Trim()
            }
            elseif ($line -match '^\s+key:\s*(.+)') {
                $currentKey = $Matches[1].Trim()
            }
            elseif ($line -match '^\s+value:\s*(.+)') {
                $currentValue = $Matches[1].Trim()
                $currentValue = $currentValue -replace '^["'']|["'']$', ''
            }
            elseif ($line -match '^\s+(when|tags):') {
                if ($currentSection -and $currentKey -and $currentValue) {
                    if ($currentValue -match '\{\{\s*([^}]+)\s*\}\}') {
                        $varName = $Matches[1].Trim()
                        $currentValue = Get-CISDefaultValue -VariableName $varName
                    }
                    
                    $omaUri = Convert-SecurityPolicyToOMAURI -Section $currentSection -Key $currentKey
                    
                    $results += @{
                        ControlId = $controlId
                        Type = "SecurityPolicy"
                        Section = $currentSection
                        Key = $currentKey
                        Value = $currentValue
                        OMAUri = $omaUri
                        Level1 = $hasLevel1
                        Level2 = $hasLevel2
                        SourceFile = $YAMLPath
                    }
                }
                $inSecurityPolicyBlock = $false
            }
        }
    }
    
    # Handle last security policy block
    if ($inSecurityPolicyBlock -and $currentSection -and $currentKey -and $currentValue) {
        if ($currentValue -match '\{\{\s*([^}]+)\s*\}\}') {
            $varName = $Matches[1].Trim()
            $currentValue = Get-CISDefaultValue -VariableName $varName
        }
        
        $omaUri = Convert-SecurityPolicyToOMAURI -Section $currentSection -Key $currentKey
        
        $results += @{
            ControlId = $controlId
            Type = "SecurityPolicy"
            Section = $currentSection
            Key = $currentKey
            Value = $currentValue
            OMAUri = $omaUri
            Level1 = $hasLevel1
            Level2 = $hasLevel2
            SourceFile = $YAMLPath
        }
    }
    
    
    # Extract user rights assignments (win_user_right) - improved regex
    $userRightPattern = '(?:ansible\.windows\.win_user_right|win_user_right):\s*\n\s*name:\s*([^\n]+)\s*\n\s*users:\s*\n((?:\s*-\s+[^\n]+\n?)+)'
    $userRightMatches = [regex]::Matches($content, $userRightPattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
    foreach ($match in $userRightMatches) {
        $rightName = $match.Groups[1].Value.Trim()
        $usersBlock = $match.Groups[2].Value
        $users = ($usersBlock -split '\n' | Where-Object { $_ -match '^\s*-\s+' } | ForEach-Object { $_ -replace '^\s*-\s+', '' }).Trim()
        
        # Convert user right to OMA-URI
        $omaUri = Convert-UserRightToOMAURI -RightName $rightName
        
        $results += @{
            ControlId = $controlId
            Type = "UserRight"
            RightName = $rightName
            Users = $users
            OMAUri = $omaUri
            Level1 = $hasLevel1
            Level2 = $hasLevel2
            SourceFile = $YAMLPath
        }
    }
    
    # Extract service settings (win_service)
    $servicePattern = '(?:ansible\.windows\.win_service|win_service):\s*\n\s*name:\s*([^\n]+)\s*\n\s*start_mode:\s*([^\n]+)'
    $serviceMatches = [regex]::Matches($content, $servicePattern, [System.Text.RegularExpressions.RegexOptions]::Multiline)
    foreach ($match in $serviceMatches) {
        $serviceName = $match.Groups[1].Value.Trim()
        $startMode = $match.Groups[2].Value.Trim()
        
        # Convert service to registry setting (services are managed via registry)
        $registryPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName"
        $registryName = "Start"
        $registryValue = switch ($startMode.ToLower()) {
            "disabled" { "4" }
            "manual" { "3" }
            "automatic" { "2" }
            default { "4" }
        }
        
        $omaUri = Convert-RegistryPathToOMAURI -RegistryPath $registryPath -RegistryName $registryName
        
        $results += @{
            ControlId = $controlId
            Type = "Service"
            ServiceName = $serviceName
            StartMode = $startMode
            RegistryPath = $registryPath
            RegistryName = $registryName
            Value = $registryValue
            DataType = "dword"
            OMAUri = $omaUri
            Level1 = $hasLevel1
            Level2 = $hasLevel2
            SourceFile = $YAMLPath
        }
    }
    
    # Extract audit policy settings (win_shell with AuditPol commands) - Section 17
    $auditPolPattern = 'AuditPol\s+/set\s+/subcategory:"([^"]+)"\s+/(success|failure):(enable|disable)'
    $auditPolMatches = [regex]::Matches($content, $auditPolPattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
    foreach ($match in $auditPolMatches) {
        $subcategory = $match.Groups[1].Value.Trim()
        $auditType = $match.Groups[2].Value.Trim()
        $auditState = $match.Groups[3].Value.Trim()
        
        # Convert audit policy to registry setting (audit policies are stored in registry)
        # Audit policies are typically configured via Group Policy or Intune Scripts
        # For now, we'll note these as requiring scripts
        $results += @{
            ControlId = $controlId
            Type = "AuditPolicy"
            Subcategory = $subcategory
            AuditType = $auditType
            State = $auditState
            RequiresScript = $true
            Level1 = $hasLevel1
            Level2 = $hasLevel2
            SourceFile = $YAMLPath
        }
    }
    
    return $results
}

function Convert-RegistryPathToOMAURI {
    param(
        [string]$RegistryPath,
        [string]$RegistryName
    )
    
    # Convert HKLM:\Software\Policies\Microsoft\... to OMA-URI
    if ($RegistryPath -match '^HKLM:\\Software\\Policies\\(.+)') {
        $policyPath = $Matches[1] -replace '\\', '/' -replace ' ', ''
        return "./Device/Vendor/MSFT/Policy/Config/$policyPath~$RegistryName"
    }
    
    # Convert HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\... to OMA-URI
    if ($RegistryPath -match '^HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\(.+)') {
        $policyPath = $Matches[1] -replace '\\', '/' -replace ' ', ''
        return "./Device/Vendor/MSFT/Policy/Config/$policyPath~$RegistryName"
    }
    
    # Convert HKCU:\... to OMA-URI (user-level policies)
    if ($RegistryPath -match '^HKCU:\\(.+)') {
        $userPath = $Matches[1] -replace '\\', '/' -replace ' ', ''
        return "./User/Vendor/MSFT/Policy/Config/$userPath~$RegistryName"
    }
    
    # Convert HKLM:\SYSTEM\CurrentControlSet\Services\... to OMA-URI (for services)
    if ($RegistryPath -match '^HKLM:\\SYSTEM\\CurrentControlSet\\Services\\([^\\]+)\\Start$') {
        $serviceName = $Matches[1]
        return "./Device/Vendor/MSFT/Policy/Config/Services~$serviceName~Start"
    }
    
    # Convert HKLM:\SYSTEM\CurrentControlSet\Control\... to OMA-URI
    if ($RegistryPath -match '^HKLM:\\SYSTEM\\CurrentControlSet\\Control\\(.+)') {
        $controlPath = $Matches[1] -replace '\\', '/' -replace ' ', ''
        return "./Device/Vendor/MSFT/Policy/Config/System~$controlPath~$RegistryName"
    }
    
    # Convert HKLM:\SYSTEM\CurrentControlSet\Services\... to OMA-URI
    if ($RegistryPath -match '^HKLM:\\SYSTEM\\CurrentControlSet\\Services\\(.+)') {
        $servicePath = $Matches[1] -replace '\\', '/' -replace ' ', ''
        return "./Device/Vendor/MSFT/Policy/Config/Services~$servicePath~$RegistryName"
    }
    
    # Fallback: use custom configuration
    return "./Device/Vendor/MSFT/CustomPolicy/$RegistryName"
}

function Get-CISDefaultValue {
    param([string]$VariableName)
    
    # Common CIS default values from defaults/main.yml
    $defaults = @{
        "win11cis_max_passwords_saved" = "24"
        "win11cis_maximum_password_age" = "365"
        "win11cis_minimum_password_age" = "1"
        "win11cis_minimum_password_length" = "14"
        "win11cis_account_lockout_threshold" = "5"
        "win11cis_account_lockout_duration" = "15"
        "win11cis_reset_account_lockout_counter_after" = "15"
    }
    
    if ($defaults.ContainsKey($VariableName)) {
        return $defaults[$VariableName]
    } else {
        return "0"
    }
}

function Convert-SecurityPolicyToOMAURI {
    param(
        [string]$Section,
        [string]$Key
    )
    
    # Map security policy sections to OMA-URI
    $sectionMap = @{
        "System Access" = "SystemAccess"
        "Kerberos Policy" = "Kerberos"
    }
    
    $mappedSection = if ($sectionMap.ContainsKey($Section)) { $sectionMap[$Section] } else { $Section }
    return "./Device/Vendor/MSFT/Policy/Config/$mappedSection~$Key"
}

function Convert-UserRightToOMAURI {
    param(
        [string]$RightName
    )
    
    # Map user rights to OMA-URI
    $rightMap = @{
        "SeNetworkLogonRight" = "UserRights/AccessThisComputerFromNetwork"
        "SeInteractiveLogonRight" = "UserRights/AllowLogOnLocally"
        "SeRemoteInteractiveLogonRight" = "UserRights/AllowLogOnThroughRemoteDesktopServices"
        "SeBackupPrivilege" = "UserRights/BackupFilesAndDirectories"
        "SeRestorePrivilege" = "UserRights/RestoreFilesAndDirectories"
        "SeSystemTimePrivilege" = "UserRights/ChangeSystemTime"
        "SeCreateTokenPrivilege" = "UserRights/CreateToken"
        "SeDebugPrivilege" = "UserRights/DebugPrograms"
        "SeDenyNetworkLogonRight" = "UserRights/DenyAccessFromNetwork"
        "SeDenyInteractiveLogonRight" = "UserRights/DenyLogOnLocally"
        "SeDenyRemoteInteractiveLogonRight" = "UserRights/DenyLogOnThroughRemoteDesktopServices"
        "SeEnableDelegationPrivilege" = "UserRights/EnableDelegation"
        "SeRemoteShutdownPrivilege" = "UserRights/ForceRemoteShutdown"
        "SeAuditPrivilege" = "UserRights/GenerateSecurityAudits"
        "SeImpersonatePrivilege" = "UserRights/ImpersonateClient"
        "SeIncreaseWorkingSetPrivilege" = "UserRights/IncreaseWorkingSet"
        "SeIncreaseBasePriorityPrivilege" = "UserRights/IncreaseSchedulingPriority"
        "SeLoadDriverPrivilege" = "UserRights/LoadUnloadDeviceDrivers"
        "SeLockMemoryPrivilege" = "UserRights/LockPagesInMemory"
        "SeMachineAccountPrivilege" = "UserRights/AddWorkstationsToDomain"
        "SeManageVolumePrivilege" = "UserRights/ManageVolume"
        "SeProfileSingleProcessPrivilege" = "UserRights/ProfileSingleProcess"
        "SeRelabelPrivilege" = "UserRights/ModifyObjectLabels"
        "SeShutdownPrivilege" = "UserRights/ShutDownSystem"
        "SeSyncAgentPrivilege" = "UserRights/SynchronizeDirectoryServiceData"
        "SeTakeOwnershipPrivilege" = "UserRights/TakeOwnership"
        "SeTcbPrivilege" = "UserRights/ActAsPartOfOperatingSystem"
        "SeTrustedCredManAccessPrivilege" = "UserRights/AccessCredentialManagerAsTrustedCaller"
        "SeUndockPrivilege" = "UserRights/RemoveComputerFromDockingStation"
        "SeCreateGlobalPrivilege" = "UserRights/CreateGlobalObjects"
        "SeCreatePermanentPrivilege" = "UserRights/CreatePermanentSharedObjects"
        "SeCreateSymbolicLinkPrivilege" = "UserRights/CreateSymbolicLinks"
        "SeIncreaseQuotaPrivilege" = "UserRights/AdjustMemoryQuotasForProcess"
    }
    
    $mappedRight = if ($rightMap.ContainsKey($RightName)) { $rightMap[$RightName] } else { "UserRights/$RightName" }
    return "./Device/Vendor/MSFT/Policy/Config/$mappedRight"
}