Exporters/Export-JUnitXml.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    JUnit XML exporter — for Azure DevOps, Jenkins, and CI/CD pipelines.
.DESCRIPTION
    Generates JUnit-compatible XML test results from BSI compliance checks.
    Compatible with dorny/test-reporter (GitHub Actions) and Azure DevOps test reporting.
.LINK
    https://github.com/dorny/test-reporter
#>

function Export-JUnitXml {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][array]$Results,
        [string]$OutputPath = '',
        [string]$TestSuiteName = 'BSI IT-Grundschutz++ Compliance'
    )

    $summary = Get-BsiSummary
    $timestamp = Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'

    # Group by category for test suites
    $grouped = $Results | Group-Object -Property Category

    $suiteXml = [System.Collections.ArrayList]::new()

    foreach ($group in $grouped) {
        $testCases = [System.Collections.ArrayList]::new()
        $failures  = 0
        $errors    = 0

        foreach ($r in $group.Group) {
            $escapedName = [System.Security.SecurityElement]::Escape("$($r.ControlId) - $($r.ControlTitle)")
            $className   = [System.Security.SecurityElement]::Escape($r.Category)

            if ($r.Status -eq [BsiCheckStatus]::Pass -or $r.Status -eq [BsiCheckStatus]::Skip) {
                $null = $testCases.Add(" <testcase name=`"$escapedName`" classname=`"$className`" time=`"0`" />")
            } else {
                $failures++
                $msg = [System.Security.SecurityElement]::Escape($r.Details)
                if ($r.Remediation) { $msg += " | $($r.Remediation)" }
                $msg = [System.Security.SecurityElement]::Escape($msg)
                $null = $testCases.Add(" <testcase name=`"$escapedName`" classname=`"$className`" time=`"0`">
        <failure message=`"BSI control $($r.ControlId) $($r.Status.ToString())`">$msg</failure>
      </testcase>"
)
            }
        }

        $suiteName = [System.Security.SecurityElement]::Escape($group.Name)
        $testCount = $group.Group.Count

        $null = $suiteXml.Add(" <testsuite name=`"$suiteName`" tests=`"$testCount`" failures=`"$failures`" errors=`"$errors`" timestamp=`"$timestamp`">")
        foreach ($tc in $testCases) { $null = $suiteXml.Add($tc) }
        $null = $suiteXml.Add(" </testsuite>")
    }

    $xml = @"
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="$([System.Security.SecurityElement]::Escape($TestSuiteName))" tests="$($summary.Total)" failures="$($summary.Failed)" errors="0">
$suiteXml
</testsuites>
"@


    if ($OutputPath) {
        $dir = Split-Path -Parent $OutputPath
        if ($dir -and -not (Test-Path -LiteralPath $dir)) {
            New-Item -ItemType Directory -Path $dir -Force | Out-Null
        }
        $xml | Set-Content -Path $OutputPath -Encoding UTF8
        Write-Verbose "[Export-JUnitXml] Written to $OutputPath"
    }

    return $xml
}