CAREPORT.psm1

function Get-AccessTokenInteractive {
    param([string]$ClientId, [string]$TenantId)
    
    try {
        # OAuth endpoints
        $authorizeUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/authorize"
        $tokenUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
        $scope = "https://graph.microsoft.com/Policy.Read.All https://graph.microsoft.com/Directory.Read.All"
        
        # Start lokale HTTP server op willekeurige poort
        $listener = New-Object System.Net.HttpListener
        $port = Get-Random -Minimum 8000 -Maximum 9000
        $redirectUri = "http://localhost:$port/"
        $listener.Prefixes.Add($redirectUri)
        
        try {
            $listener.Start()
        } catch {
            # Probeer andere poort als deze bezet is
            $port = Get-Random -Minimum 9000 -Maximum 10000
            $redirectUri = "http://localhost:$port/"
            $listener.Prefixes.Clear()
            $listener.Prefixes.Add($redirectUri)
            $listener.Start()
        }
        
        Write-Host "Interactieve login starten..." -ForegroundColor Cyan
        
        # Bouw authorization URL met prompt=consent om altijd consent te vragen
        $state = [guid]::NewGuid().ToString()
        $authUrl = "$authorizeUrl" + 
            "?client_id=$ClientId" + 
            "&response_type=code" + 
            "&redirect_uri=$([uri]::EscapeDataString($redirectUri))" + 
            "&scope=$([uri]::EscapeDataString($scope))" + 
            "&state=$state" + 
            "&response_mode=query" +
            "&prompt=consent"
        
        Write-Host "Browser wordt geopend - log in en keur permissions goed" -ForegroundColor Yellow
        Start-Process $authUrl
        
        # Wacht op OAuth callback
        Write-Host "Wachten op login en consent..." -ForegroundColor Gray
        $context = $listener.GetContext()
        $request = $context.Request
        $response = $context.Response
        
        # Parse query parameters
        $query = $request.Url.Query
        if ($query -match "code=([^&]+)") {
            $authCode = $matches[1]
            
            # Sluit de HTTP response direct af zonder HTML content
            $response.StatusCode = 204  # No Content
            $response.Close()
            $listener.Stop()
            
            Write-Host "Login en consent succesvol!" -ForegroundColor Green
            
            # Exchange authorization code voor access token
            $tokenBody = @{
                client_id = $ClientId
                scope = $scope
                code = $authCode
                redirect_uri = $redirectUri
                grant_type = "authorization_code"
            }
            
            $tokenResponse = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $tokenBody -ContentType "application/x-www-form-urlencoded"
            
            if ($tokenResponse.access_token) {
                return $tokenResponse.access_token
            } else {
                throw "Geen access token ontvangen"
            }
            
        } elseif ($query -match "error=([^&]+)") {
            $error = $matches[1]
            $listener.Stop()
            throw "OAuth error: $error"
        } else {
            $listener.Stop()
            throw "Login geannuleerd"
        }
        
    } catch {
        if ($listener -and $listener.IsListening) { 
            $listener.Stop() 
        }
        throw "Interactieve authenticatie gefaald: $($_.Exception.Message)"
    }
}

function Export-CAPolicies {
    param([string]$AccessToken, [string]$ExportPath)
    
    try {
        $headers = @{
            'Authorization' = "Bearer $AccessToken"
            'Content-Type' = 'application/json'
        }
        
        $uri = "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies"
        $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get
        
        if (-not $response -or -not $response.value) {
            return @()
        }
        
        if ($response.value.Count -eq 0) {
            return @()
        }
        
        Write-Host "$($response.value.Count) policies gevonden" -ForegroundColor Green
        
        $exportedCount = 0
        
        foreach ($policy in $response.value) {
            try {
                $fileName = $policy.displayName -replace '[<>:"/\\|?*]', '_'
                $fileName = "$fileName.json"
                $filePath = Join-Path $ExportPath $fileName
                
                $policyJson = $policy | ConvertTo-Json -Depth 8
                $policyJson | Out-File -FilePath $filePath -Encoding UTF8 -Force
                
                $exportedCount++
            } catch {
                Write-Warning "Export fout: $($policy.displayName)"
            }
        }
        
        Write-Host "$exportedCount policies geexporteerd" -ForegroundColor Green
        return $response.value
    } catch {
        if ($_.Exception.Response) {
            $statusCode = $_.Exception.Response.StatusCode
            switch ($statusCode) {
                'Forbidden' { throw "Onvoldoende permissies - controleer Policy.Read.All en Directory.Read.All permissions" }
                'Unauthorized' { throw "Token ongeldig - probeer opnieuw in te loggen" }
                default { throw "Graph API fout: $statusCode" }
            }
        } else {
            throw "Export gefaald: $($_.Exception.Message)"
        }
    }
}

# Zero Trust helper functions
function Get-RadarPoint {
    param(
        [int]$index,
        [int]$totalPoints,
        [int]$value,
        [int]$maxValue,
        [int]$radius
    )
    $angle = (2 * [Math]::PI / $totalPoints) * $index
    $scale = if ($maxValue -eq 0) { 0 } else { $value / $maxValue }
    $r = $scale * $radius
    $x = 280 + ($r * [Math]::Sin($angle))
    $y = 210 - ($r * [Math]::Cos($angle))
    return "$x,$y"
}

function Get-VerifyExplicitlyScore($policy) {
    $mfaScore    = if ($policy.grantControls.builtInControls -contains "mfa") { 100 } else { 0 }
    $riskScore   = if ($policy.conditions.userRiskLevels -or $policy.conditions.signInRiskLevels) { 100 } else { 0 }
    $deviceScore = if ($policy.grantControls.builtInControls -contains "compliantDevice") { 100 } else { 0 }
    return @{ MFA = $mfaScore; Risk = $riskScore; Device = $deviceScore }
}

function Get-LeastPrivilegeAssumeBreachScore($policy) {
    $granularScore   = if ($policy.conditions.users.includeUsers -ne "All" -or $policy.conditions.users.includeGroups) { 100 } else { 0 }
    $appScore        = if ($policy.conditions.applications.includeApplications -ne "All") { 100 } else { 0 }
    $locationScore   = if ($policy.conditions.locations) { 100 } else { 0 }
    $sessionScore    = if ($policy.sessionControls) { 100 } else { 0 }
    $blockLegacyScore= if ($policy.conditions.clientAppTypes -and !($policy.conditions.clientAppTypes -contains "exchangeActiveSync")) { 100 } else { 0 }
    return @{ Granular = $granularScore; Apps = $appScore; Location = $locationScore; Session = $sessionScore; BlockLegacy = $blockLegacyScore }
}

function Get-ThresholdClass([int]$p) {
    if ($p -ge 80) { 'green' }
    elseif ($p -ge 50) { 'amber' }
    else { 'red' }
}

function Get-ZeroTrustCategories($policy) {
    $cats = @()
    if ($policy.grantControls.builtInControls -contains "mfa" -or
        $policy.conditions.userRiskLevels -or
        $policy.conditions.signInRiskLevels -or
        $policy.grantControls.builtInControls -contains "compliantDevice") {
        $cats += "Verify Explicitly"
    }
    if (($policy.conditions.users.includeUsers -ne "All" -or $policy.conditions.users.includeGroups) -or
        ($policy.conditions.applications.includeApplications -ne "All")) {
        $cats += "Least Privilege"
    }
    if ($policy.conditions.locations -or
        $policy.sessionControls -or
        ($policy.conditions.clientAppTypes -and !($policy.conditions.clientAppTypes -contains "exchangeActiveSync"))) {
        $cats += "Assume Breach"
    }
    return $cats
}

function New-CAReport {
    param([array]$Policies, [string]$OutputPath)
    
    if ($Policies.Count -eq 0) {
        throw "Geen policies om rapport van te maken"
    }
    
    # Statistieken berekenen
    $totalPolicies     = $Policies.Count
    $enabledPolicies   = ($Policies | Where-Object { $_.state -eq "enabled" }).Count
    $disabledPolicies  = ($Policies | Where-Object { $_.state -eq "disabled" }).Count
    $reportOnlyPolicies= ($Policies | Where-Object { $_.state -eq "enabledForReportingButNotEnforced" }).Count
    $blockPolicies     = ($Policies | Where-Object { $_.grantControls.builtInControls -contains "block" }).Count
    $mfaPolicies       = ($Policies | Where-Object { $_.grantControls.builtInControls -contains "mfa" }).Count

    # Zero Trust scores berekenen
    $verifyExplicitlyScores = @{ MFA = 0; Risk = 0; Device = 0 }
    $leastPrivilegeScores   = @{ Granular = 0; Apps = 0; Location = 0; Session = 0; BlockLegacy = 0 }

    foreach ($policy in $Policies) {
        if ($policy.state -eq "enabled") {
            $ve = Get-VerifyExplicitlyScore $policy
            $lp = Get-LeastPrivilegeAssumeBreachScore $policy
            $verifyExplicitlyScores.MFA   += $ve.MFA
            $verifyExplicitlyScores.Risk  += $ve.Risk
            $verifyExplicitlyScores.Device+= $ve.Device
            $leastPrivilegeScores.Granular+= $lp.Granular
            $leastPrivilegeScores.Apps    += $lp.Apps
            $leastPrivilegeScores.Location+= $lp.Location
            $leastPrivilegeScores.Session += $lp.Session
            $leastPrivilegeScores.BlockLegacy += $lp.BlockLegacy
        }
    }

    $activeCount = $enabledPolicies
    if ($activeCount -gt 0) {
        $veNormalized = @{
            MFA    = [math]::Round($verifyExplicitlyScores.MFA    / $activeCount)
            Risk   = [math]::Round($verifyExplicitlyScores.Risk   / $activeCount)
            Device = [math]::Round($verifyExplicitlyScores.Device / $activeCount)
        }
        $lpNormalized = @{
            Granular    = [math]::Round($leastPrivilegeScores.Granular    / $activeCount)
            Apps        = [math]::Round($leastPrivilegeScores.Apps        / $activeCount)
            Location    = [math]::Round($leastPrivilegeScores.Location    / $activeCount)
            Session     = [math]::Round($leastPrivilegeScores.Session     / $activeCount)
            BlockLegacy = [math]::Round($leastPrivilegeScores.BlockLegacy / $activeCount)
        }
    } else {
        $veNormalized = @{ MFA = 0; Risk = 0; Device = 0 }
        $lpNormalized = @{ Granular = 0; Apps = 0; Location = 0; Session = 0; BlockLegacy = 0 }
    }

    # Zero Trust pilaren (0-100)
    $pillarVerify         = [math]::Round(($veNormalized.MFA + $veNormalized.Risk + $veNormalized.Device) / 3)
    $pillarLeastPrivilege = [math]::Round(($lpNormalized.Granular + $lpNormalized.Apps) / 2)
    $pillarAssumeBreach   = [math]::Round(($lpNormalized.Location + $lpNormalized.Session + $lpNormalized.BlockLegacy) / 3)

    # Badge kleuren
    $verifyClass = Get-ThresholdClass $pillarVerify
    $leastClass  = Get-ThresholdClass $pillarLeastPrivilege
    $breachClass = Get-ThresholdClass $pillarAssumeBreach

    # Spider chart coördinaten
    $ztPoint1  = Get-RadarPoint -index 0 -totalPoints 3 -value $pillarVerify -maxValue 100 -radius 180
    $ztPoint2  = Get-RadarPoint -index 1 -totalPoints 3 -value $pillarLeastPrivilege -maxValue 100 -radius 180
    $ztPoint3  = Get-RadarPoint -index 2 -totalPoints 3 -value $pillarAssumeBreach -maxValue 100 -radius 180
    $ztPolygon = "$ztPoint1 $ztPoint2 $ztPoint3"
    
    $reportPath = Join-Path $OutputPath "CAREPORT-Report-$(Get-Date -Format 'yyyyMMdd-HHmmss').html"
    
    # HTML genereren (nu met de geavanceerde layout uit het voorbeeld)
    $htmlContent = @"
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CAREPORT - Conditional Access Report</title>
<style>
*{box-sizing:border-box}
body{font-family:Arial,sans-serif;color:#333;background:#fff;margin:0;padding:20px;line-height:1.4}
.container{max-width:1200px;margin:0 auto;background:#fff;border:1px solid #ccc;border-radius:4px;padding:30px}
.header{border-bottom:2px solid #ddd;padding-bottom:15px;margin-bottom:30px;position:relative;padding-right:160px;min-height:65px;display:flex;flex-direction:column;justify-content:center}
.header h1{margin:0;font-weight:600;font-size:24px;color:#333}
.header .subtitle{color:#666;font-size:14px;margin-top:5px}
/* Stat cards */
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:30px}
.stat-card{background:#f8f8f8;border:1px solid #ddd;border-radius:4px;padding:20px;text-align:center}
.stat-number{font-size:2.4em;font-weight:700;color:#222;margin-bottom:5px}
.stat-label{font-size:13px;color:#666;font-weight:500;text-transform:uppercase;letter-spacing:.5px}
/* Spider sectie */
.charts-section{background:#f8f8f8;border:1px solid #ddd;border-radius:4px;padding:20px;margin-bottom:30px}
.charts-section h2{margin:0 0 12px 0;color:#333;font-size:18px;font-weight:600;text-align:center}
.chart-item{text-align:center}
/* Badges */
.badge-row{display:flex;justify-content:center;gap:10px;margin:-5px 0 12px 0;flex-wrap:wrap}
.badge{padding:4px 10px;border-radius:14px;font-size:12px;font-weight:700;color:#fff}
.badge.green{background:#2e7d32}
.badge.amber{background:#f9a825}
.badge.red{background:#c62828}
/* Zero Trust category badges */
.zt-badge {
  background: #dedede;
  color: #1a237e;
  font-size: 11px;
  font-weight: bold;
  border-radius: 8px;
  padding: 2px 8px;
  margin-right: 4px;
  display: inline-block;
  margin-bottom: 2px;
}
/* SVG text verbeteringen */
svg text{fill:#222 !important;font-weight:bold !important;font-size:14px !important}
polygon{fill-opacity:0.35 !important;stroke-width:2 !important}
/* Filters */
.filter-bar{background:#f8f8f8;border:1px solid #ddd;border-radius:4px;padding:15px 20px;margin-bottom:20px;display:flex;align-items:center;gap:20px;flex-wrap:wrap}
.filter-group{display:flex;align-items:center;gap:8px}
.filter-group label{font-weight:500;color:#555;font-size:14px;white-space:nowrap}
.filter-group select,.filter-group input{padding:6px 10px;border:1px solid #ccc;border-radius:3px;font-size:14px;background:#fff;min-width:150px}
.filter-group input[type=text]{min-width:200px}
.filter-group select:focus,.filter-group input:focus{outline:none;border-color:#0d47a1;box-shadow:0 0 3px rgba(13,71,161,.2)}
/* Tabel */
.table-container{background:#fff;border:1px solid #ddd;border-radius:4px;overflow:hidden}
table{width:100%;border-collapse:collapse;font-size:14px}
th{background:#f0f0f0;color:#333;padding:12px 15px;text-align:left;font-weight:600;border-bottom:2px solid #ccc}
td{padding:12px 15px;border-bottom:1px solid #eee;vertical-align:middle}
tr:hover{background:#f9f9f9}
tr:last-child td{border-bottom:none}
/* Status */
.status{padding:4px 8px;border-radius:3px;font-size:11px;font-weight:600;text-transform:uppercase;color:#fff;letter-spacing:.3px}
.status-enabled{background:#2d5a2d}
.status-disabled{background:#666}
.status-enabledForReportingButNotEnforced{background:#8c6d00}
/* Links */
.policy-link{color:#0d47a1;text-decoration:none;font-weight:500;cursor:pointer;border:none;background:none;padding:0;font-size:inherit;text-align:left;width:100%}
.policy-link:hover{color:#1565c0;text-decoration:underline}
/* Modal */
.modal{display:none;position:fixed;z-index:1000;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgba(0,0,0,.4)}
.modal-content{background:#fff;margin:3% auto;padding:0;border:1px solid #ccc;width:90%;max-width:900px;border-radius:4px}
.modal-header{padding:15px 20px;background:#f0f0f0;border-bottom:1px solid #ddd;display:flex;justify-content:space-between;align-items:center}
.modal-header h2{margin:0;font-size:18px;font-weight:600;color:#333}
.close{color:#666;font-size:24px;font-weight:bold;cursor:pointer;border:none;background:none;padding:0}
.close:hover{color:#333}
.modal-body{padding:20px;max-height:70vh;overflow-y:auto}
.json-container{background:#f8f8f8;border:1px solid #ddd;border-radius:3px;padding:15px;font-family:'Courier New',monospace;font-size:13px;line-height:1.4;white-space:pre-wrap;word-wrap:break-word;color:#333}
/* Details */
.policy-details{font-family:Arial,sans-serif}
.detail-section{margin-bottom:25px;background:#f8f8f8;border-radius:4px;padding:15px;border-left:3px solid #0d47a1}
.detail-section h3{margin:0 0 15px 0;color:#333;font-size:16px;font-weight:600}
.detail-grid{display:grid;gap:8px}
.detail-row{display:grid;grid-template-columns:180px 1fr;gap:15px;align-items:start;padding:6px 0;border-bottom:1px solid #eee}
.detail-row:last-child{border-bottom:none}
.detail-label{font-weight:600;color:#555;font-size:13px}
.detail-value{color:#333;font-size:13px}
.empty-value{color:#999;font-style:italic}
.status-badge{padding:3px 8px;border-radius:3px;font-size:11px;font-weight:600;text-transform:uppercase;color:#fff}
/* Footer */
.footer{text-align:center;margin-top:40px;padding-top:20px;border-top:1px solid #eee;color:#666;font-size:12px}
/* Responsive */
@media (max-width:768px){
 .stats-grid{grid-template-columns:repeat(2,1fr)}
 .header{padding-right:10px;flex-direction:column;align-items:flex-start;min-height:auto}
 .filter-bar{flex-direction:column;align-items:stretch;gap:15px}
 .filter-group{justify-content:space-between}
 table{font-size:12px}
 th,td{padding:8px 10px}
 .detail-row{grid-template-columns:1fr;gap:5px}
}
</style>
</head>
<body>
<div class="container">
  <div class="header">
    <h1>CAREPORT - Conditional Access Report</h1>
    <div class="subtitle">Rapport gegenereerd op $(Get-Date -Format "dd MMMM yyyy HH:mm")</div>
  </div>
 
  <div class="stats-grid">
    <div class="stat-card"><div class="stat-number">$totalPolicies</div><div class="stat-label">Totaal Policies</div></div>
    <div class="stat-card"><div class="stat-number">$enabledPolicies</div><div class="stat-label">Actief</div></div>
    <div class="stat-card"><div class="stat-number">$disabledPolicies</div><div class="stat-label">Uitgeschakeld</div></div>
    <div class="stat-card"><div class="stat-number">$reportOnlyPolicies</div><div class="stat-label">Rapport Modus</div></div>
  </div>
 
  <!-- Zero Trust Spider Chart -->
  <div class="charts-section">
    <h2>Zero Trust Compliance</h2>
    <div class="badge-row">
      <span class="badge $verifyClass">Verify Explicitly: $pillarVerify%</span>
      <span class="badge $leastClass">Least Privilege: $pillarLeastPrivilege%</span>
      <span class="badge $breachClass">Assume Breach: $pillarAssumeBreach%</span>
    </div>
    <div class="chart-item" style="max-width:560px;margin:0 auto;">
      <svg width="560" height="420" viewBox="0 0 560 420" style="max-width:100%;height:auto;">
        <!-- Grid -->
        <g stroke="#eeeeee" stroke-width="1" fill="none">
          <circle cx="280" cy="210" r="180"/>
          <circle cx="280" cy="210" r="144"/>
          <circle cx="280" cy="210" r="108"/>
          <circle cx="280" cy="210" r="72"/>
          <circle cx="280" cy="210" r="36"/>
        </g>
        <!-- Axes -->
        <g stroke="#cccccc" stroke-width="1">
          <line x1="280" y1="30" x2="280" y2="210"/>
          <line x1="436" y1="322" x2="280" y2="210"/>
          <line x1="124" y1="322" x2="280" y2="210"/>
        </g>
        <!-- Polygon -->
        <polygon points="$ztPolygon" fill="rgba(13,71,161,0.35)" stroke="rgba(13,71,161,1)" stroke-width="2"/>
        <!-- Points -->
        <circle cx="$($ztPoint1 -split ',')[0]" cy="$($ztPoint1 -split ',')[1]" r="4" fill="rgba(13,71,161,1)"/>
        <circle cx="$($ztPoint2 -split ',')[0]" cy="$($ztPoint2 -split ',')[1]" r="4" fill="rgba(13,71,161,1)"/>
        <circle cx="$($ztPoint3 -split ',')[0]" cy="$($ztPoint3 -split ',')[1]" r="4" fill="rgba(13,71,161,1)"/>
         
        <!-- Labels -->
        <text x="280" y="20" text-anchor="middle" font-family="Arial" font-size="12" fill="#555">Verify Explicitly</text>
        <text x="456" y="335" text-anchor="middle" font-family="Arial" font-size="12" fill="#555">Least Privilege</text>
        <text x="104" y="335" text-anchor="middle" font-family="Arial" font-size="12" fill="#555">Assume Breach</text>
         
        <!-- Scores -->
        <text x="280" y="165" text-anchor="middle" font-family="Arial" font-weight="bold" font-size="16" fill="#1b1b1b">$pillarVerify%</text>
        <text x="350" y="270" text-anchor="middle" font-family="Arial" font-weight="bold" font-size="16" fill="#1b1b1b">$pillarLeastPrivilege%</text>
        <text x="210" y="270" text-anchor="middle" font-family="Arial" font-weight="bold" font-size="16" fill="#1b1b1b">$pillarAssumeBreach%</text>
      </svg>
    </div>
  </div>
 
  <!-- Filters -->
  <div class="filter-bar">
    <div class="filter-group">
      <label for="statusFilter">Filter op status:</label>
      <select id="statusFilter" onchange="filterPolicies()">
        <option value="">Alle statussen</option>
        <option value="enabled">Actief</option>
        <option value="disabled">Uitgeschakeld</option>
        <option value="enabledForReportingButNotEnforced">Rapport Modus</option>
      </select>
    </div>
    <div class="filter-group">
      <label for="controlFilter">Filter op control type:</label>
      <select id="controlFilter" onchange="filterPolicies()">
        <option value="">Alle types</option>
        <option value="block">Block</option>
        <option value="mfa">MFA</option>
        <option value="compliant">Device Compliance</option>
        <option value="allow">Allow</option>
      </select>
    </div>
    <div class="filter-group">
      <label for="searchFilter">Zoeken:</label>
      <input type="text" id="searchFilter" placeholder="Policy naam..." onkeyup="filterPolicies()">
    </div>
  </div>
 
  <!-- Tabel -->
  <div class="table-container">
    <table id="policyTable">
      <thead>
        <tr>
          <th>Policy Naam</th>
          <th>Status</th>
          <th>Zero Trust Categorieën</th>
          <th>Laatst Gewijzigd</th>
        </tr>
      </thead>
      <tbody>
"@


    # Tabel rijen toevoegen
    foreach ($policy in ($Policies | Sort-Object displayName)) {
        $lastModified = if ([string]::IsNullOrEmpty($policy.modifiedDateTime)) {
            'Niet beschikbaar'
        } else { 
            try { [DateTime]::Parse($policy.modifiedDateTime).ToString("dd-MM-yyyy") } 
            catch { 'Ongeldig datum formaat' } 
        }

        $policyJson = $policy | ConvertTo-Json -Depth 10 -Compress
        $policyJsonEscaped = $policyJson -replace '"','&quot;' -replace "'",'&apos;'
        
        $controlType = "allow"
        if ($policy.grantControls -and $policy.grantControls.builtInControls) {
            if ($policy.grantControls.builtInControls -contains "block") { $controlType = "block" }
            elseif ($policy.grantControls.builtInControls -contains "mfa") { $controlType = "mfa" }
            elseif ($policy.grantControls.builtInControls -contains "compliantDevice") { $controlType = "compliant" }
        }
        
        $categories = Get-ZeroTrustCategories $policy
        $catBadgesHtml = ($categories | ForEach-Object {
            "<span class='zt-badge'>" + $_ + "</span>"
        }) -join " "
        
        $htmlContent += @"
        <tr data-status="$($policy.state)" data-control="$controlType">
          <td>
            <button class="policy-link" data-policy-json="$policyJsonEscaped" data-policy-name="$($policy.displayName)">$([System.Web.HttpUtility]::HtmlEncode($policy.displayName))</button>
          </td>
          <td><span class="status status-$($policy.state)">$($policy.state)</span></td>
          <td>$catBadgesHtml</td>
          <td>$lastModified</td>
        </tr>
"@

    }

    $htmlContent += @"
      </tbody>
    </table>
  </div>
 
  <div class="footer">
    <p><strong>CAREPORT v1.0.1</strong> | Gegenereerd op $(Get-Date -Format "dd-MM-yyyy HH:mm:ss")</p>
    <p>Totaal: $totalPolicies | Actief: $enabledPolicies | Uitgeschakeld: $disabledPolicies | Rapport modus: $reportOnlyPolicies</p>
    <p>Zero Trust: Verify $pillarVerify% | Least Privilege $pillarLeastPrivilege% | Assume Breach $pillarAssumeBreach%</p>
  </div>
</div>
 
<!-- Modal -->
<div id="policyModal" class="modal">
  <div class="modal-content">
    <div class="modal-header">
      <h2 id="modalTitle">Policy Details</h2>
      <button class="close" onclick="closePolicyModal()">&times;</button>
    </div>
    <div class="modal-body">
      <div id="jsonContainer" class="json-container"></div>
    </div>
  </div>
</div>
 
<script>
function filterPolicies(){
  const statusFilter=document.getElementById('statusFilter').value.toLowerCase();
  const controlFilter=document.getElementById('controlFilter').value.toLowerCase();
  const searchFilter=document.getElementById('searchFilter').value.toLowerCase();
  const rows=document.getElementById('policyTable').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
  for(let i=0;i<rows.length;i++){
    const row=rows[i];
    const policyName=row.getElementsByTagName('td')[0].textContent.toLowerCase();
    const status=row.getAttribute('data-status').toLowerCase();
    const control=row.getAttribute('data-control').toLowerCase();
    let show=true;
    if(statusFilter && status!==statusFilter) show=false;
    if(controlFilter && control!==controlFilter) show=false;
    if(searchFilter && !policyName.includes(searchFilter)) show=false;
    row.style.display=show?'':'none';
  }
}
 
document.addEventListener('DOMContentLoaded',function(){
  const policyButtons=document.querySelectorAll('.policy-link');
  policyButtons.forEach(btn=>{
    btn.addEventListener('click',function(){
      const name=this.getAttribute('data-policy-name');
      const jsonStr=this.getAttribute('data-policy-json').replace(/&quot;/g,'"').replace(/&apos;/g,"'");
      try{
        const obj=JSON.parse(jsonStr);
        const html=convertPolicyToHtml(obj);
        openPolicyModal(name,html);
      }catch(e){
        console.error('Parse error',e); alert('Fout bij laden van policy details: '+e.message);
      }
    });
  });
});
 
function convertPolicyToHtml(policy){
  function formatArray(arr,empty){
    empty=empty||'Geen';
    if(!arr || arr.length===0) return '<span class="empty-value">'+empty+'</span>';
    return '<ul>'+arr.map(i=>'<li>'+i+'</li>').join('')+'</ul>';
  }
  function formatValue(v,empty){
    empty=empty||'Niet ingesteld';
    if(v===null||v===undefined||v==='') return '<span class="empty-value">'+empty+'</span>';
    if(Array.isArray(v)) return formatArray(v);
    return v;
  }
  function formatDate(d){
    if(!d) return 'Onbekend';
    return new Date(d).toLocaleString('nl-NL');
  }
  function getControlTypeInfo(gc){
    if(!gc || !gc.builtInControls) return {type:'Niet gedefinieerd',class:'undefined-control'};
    if(gc.builtInControls.includes('block')) return {type:'Blokkeren',class:'block-control'};
    if(gc.builtInControls.includes('mfa')) return {type:'MFA Vereist',class:'mfa-control'};
    if(gc.builtInControls.includes('compliantDevice')) return {type:'Compliant Device',class:'device-control'};
    if(gc.builtInControls.includes('domainJoinedDevice')) return {type:'Domain Joined',class:'domain-control'};
    return {type:'Toestaan',class:'allow-control'};
  }
   
  const c=getControlTypeInfo(policy.grantControls);
  let html='<div class="policy-details">';
  html+='<div class="detail-section"><h3>Algemene Informatie</h3><div class="detail-grid">';
  html+='<div class="detail-row"><span class="detail-label">Status:</span><span class="status-badge status-'+policy.state+'">'+String(policy.state||'').toUpperCase()+'</span></div>';
  html+='<div class="detail-row"><span class="detail-label">Policy ID:</span><span class="detail-value">'+formatValue(policy.id)+'</span></div>';
  html+='<div class="detail-row"><span class="detail-label">Aangemaakt:</span><span class="detail-value">'+formatDate(policy.createdDateTime)+'</span></div>';
  html+='<div class="detail-row"><span class="detail-label">Laatst gewijzigd:</span><span class="detail-value">'+formatDate(policy.modifiedDateTime)+'</span></div>';
  html+='</div></div>';
 
  html+='<div class="detail-section"><h3>Grant Controls</h3>';
  html+='<div class="control-summary '+c.class+'"><strong>Control Type:</strong> '+c.type+'</div>';
  if(policy.grantControls){
    html+='<div class="detail-grid">';
    html+='<div class="detail-row"><span class="detail-label">Operator:</span><span class="detail-value">'+(policy.grantControls.operator||'OR')+'</span></div>';
    html+='<div class="detail-row"><span class="detail-label">Built-in Controls:</span><span class="detail-value">'+formatArray(policy.grantControls.builtInControls)+'</span></div>';
    html+='</div>';
  } else {
    html+='<p class="empty-value">Geen grant controls gedefinieerd</p>';
  }
  html+='</div>';
 
  html+='<div class="detail-section"><h3>Gebruikers en Groepen</h3><div class="detail-grid">';
  html+='<div class="detail-row"><span class="detail-label">Inbegrepen gebruikers:</span><span class="detail-value">'+formatArray(policy?.conditions?.users?.includeUsers)+'</span></div>';
  html+='<div class="detail-row"><span class="detail-label">Uitgesloten gebruikers:</span><span class="detail-value">'+formatArray(policy?.conditions?.users?.excludeUsers)+'</span></div>';
  html+='<div class="detail-row"><span class="detail-label">Inbegrepen groepen:</span><span class="detail-value">'+formatArray(policy?.conditions?.users?.includeGroups)+'</span></div>';
  html+='</div></div>';
 
  html+='<div class="detail-section"><h3>Applicaties</h3><div class="detail-grid">';
  html+='<div class="detail-row"><span class="detail-label">Inbegrepen applicaties:</span><span class="detail-value">'+formatArray(policy?.conditions?.applications?.includeApplications)+'</span></div>';
  html+='<div class="detail-row"><span class="detail-label">Client App Types:</span><span class="detail-value">'+formatArray(policy?.conditions?.clientAppTypes)+'</span></div>';
  html+='</div></div>';
 
  html+='</div>';
  return html;
}
 
function openPolicyModal(name,content){
  document.getElementById('modalTitle').textContent=name;
  document.getElementById('jsonContainer').innerHTML=content;
  document.getElementById('policyModal').style.display='block';
}
function closePolicyModal(){ document.getElementById('policyModal').style.display='none'; }
window.onclick=function(e){ const m=document.getElementById('policyModal'); if(e.target===m){ closePolicyModal(); } }
document.addEventListener('keydown',function(e){ if(e.key==='Escape'){ closePolicyModal(); } });
</script>
</body>
</html>
"@


    $htmlContent | Out-File -FilePath $reportPath -Encoding UTF8 -Force
    return $reportPath
}

function Get-CA {
    param(
        [string]$ClientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e",
        [string]$TenantId = "common",
        [string]$OutputPath = [Environment]::GetFolderPath("Desktop"),
        [bool]$OpenReport = $true,
        [switch]$ExportJson
    )
    
    Write-Host "CAREPORT v1.0.1 - Conditional Access Reporter" -ForegroundColor Cyan
    Write-Host "============================================================" -ForegroundColor Gray
    
    Add-Type -AssemblyName System.Web
    $tempPath = Join-Path $env:TEMP "CAREPORT-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
    $startTime = Get-Date
    
    try {
        Write-Host "Interactieve login starten..." -ForegroundColor Cyan
        
        $accessToken = Get-AccessTokenInteractive -ClientId $ClientId -TenantId $TenantId
        
        if (-not $accessToken) {
            throw "Authenticatie mislukt"
        }
        
        Write-Host "Succesvol geauthenticeerd" -ForegroundColor Green
        
        New-Item -ItemType Directory -Path $tempPath -Force | Out-Null
        
        Write-Host "Policies ophalen..." -ForegroundColor Cyan
        $policies = Export-CAPolicies -AccessToken $accessToken -ExportPath $tempPath
        
        if ($policies.Count -eq 0) {
            Write-Warning "Geen policies gevonden"
            return
        }
        
        Write-Host "$($policies.Count) policies geexporteerd" -ForegroundColor Green
        
        Write-Host "Geavanceerd rapport genereren..." -ForegroundColor Cyan
        $reportPath = New-CAReport -Policies $policies -OutputPath $OutputPath
        
        if (-not $ExportJson) {
            Remove-Item -Path $tempPath -Recurse -Force -ErrorAction SilentlyContinue
        }
        
        $duration = (Get-Date) - $startTime
        Write-Host "============================================================" -ForegroundColor Gray
        Write-Host "Rapport voltooid!" -ForegroundColor Green
        Write-Host "Bestand: $reportPath" -ForegroundColor Green
        Write-Host "Duur: $($duration.TotalSeconds.ToString('F1'))s" -ForegroundColor Gray
        
        if ($OpenReport -and (Test-Path $reportPath)) {
            Start-Process $reportPath
        }
        
        return [PSCustomObject]@{
            ModuleName = "CAREPORT"
            Version = "1.0.1"
            ReportPath = $reportPath
            PoliciesFound = $policies.Count
            Duration = $duration
            TempPath = if ($ExportJson) { $tempPath } else { $null }
        }
        
    } catch {
        Write-Error "Fout: $($_.Exception.Message)"
        if (Test-Path $tempPath) {
            Remove-Item -Path $tempPath -Recurse -Force -ErrorAction SilentlyContinue
        }
        throw
    }
}

Export-ModuleMember -Function Get-CA