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 '"','"' -replace "'",''' $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()">×</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(/"/g,'"').replace(/'/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 |