Skip to content

feat: shared report template, CSA report, and security hardening#5

Merged
royklo merged 11 commits into
mainfrom
feature/report-rebrand-integration
May 8, 2026
Merged

feat: shared report template, CSA report, and security hardening#5
royklo merged 11 commits into
mainfrom
feature/report-rebrand-integration

Conversation

@royklo

@royklo royklo commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Shared HTML report template (Get-RKSolutionsReportTemplate) with rksolutions.nl branding — all 5 reports
    migrated, eliminating ~3000 lines of duplicated HTML/CSS/JS
  • New Get-CustomSecurityAttributesReport cmdlet — auto-discovers all attribute sets, supports users, devices,
    and enterprise apps (not just users)
  • Security hardening — XSS protection across all report generators, URI injection validation, safe JS filter
    dropdowns
  • Code quality — replaced quadratic array patterns, removed dead code, fixed silent error swallowing

What changed

New features

  • Get-CustomSecurityAttributesReport cmdlet with auto-discovery of attribute sets across users, devices, and
    service principals
  • Shared report template with consistent branding, light/dark theme support, and DataTable column width
    recalculation on tab switch

Security fixes (P0)

  • HTML-encode all Graph API data before HTML interpolation (stored XSS via display names, UPNs, etc.)
  • Fix JS filter dropdown injection — use jQuery DOM API instead of string concatenation
  • Validate -From parameter as email/GUID in Send-EmailWithAttachment

Code quality (P1)

  • Replace $array += with List[PSObject].Add() in M365License and CustomSecurityAttributes
  • Add Write-Verbose to empty catch blocks in IntuneEnrollmentFlows
  • Add Write-Warning when Graph paging silently caps at 10,000 results
  • Fix report file deletion before confirming email was sent
  • Guard undefined $mermaidDiagram variable with null-check

Maintenance (P2/P3)

  • Fix typo OperationSystemEdtionOverviewOperatingSystemEditionOverview
  • Sanitize tenant name in export paths
  • Use [System.IO.Path]::GetTempFileName() instead of hardcoded C:\temp
  • Remove unused parameters, dead OS detection code, Dutch comments
  • Fix double-encoded → HTML entity in PIM Audit Logs
  • Rename New-RKSolutionsReportTemplateGet-RKSolutionsReportTemplate

Report UI

  • PIM Audit Logs: fix column widths with table-layout:fixed and constrained overflow
  • M365 License: replace assignment type color badges with plain text

Files changed (15)

File Change
Private/Get-RKSolutionsReportTemplate.ps1 Renamed from New-*, shared template with branding
Private/CustomSecurityAttributes.ps1 Major rewrite — multi-entity, auto-discovery, XSS fixes
Private/EntraAdminRoles.ps1 XSS fixes, PIM table layout, arrow encoding fix
Private/IntuneAnomalies.ps1 XSS fixes, typo rename, temp file, try/catch, Dutch→English
Private/IntuneEnrollmentFlows.ps1 XSS fixes, Write-Verbose in catch blocks
Private/M365License.ps1 XSS fixes, List collections, plain text assignment type
Private/Invoke-GraphRequestWithPaging.ps1 Write-Warning on 10k cap
Private/Send-EmailWithAttachment.ps1 $From validation
Private/Export-Results.ps1 Remove dead OS detection code
Public/Get-CustomSecurityAttributesReport.ps1 New cmdlet
Public/Get-IntuneAnomaliesReport.ps1 Typo rename, $emailSent init, deletion fix
Public/Get-IntuneEnrollmentFlowsReport.ps1 $mermaidDiagram guard
Public/Connect-RKGraph.ps1 Minor update
RKSolutions.psd1 v1.1.0, new CSA scopes
RKSolutions.psm1 Template file rename

Test plan

  • Import-Module ./module/RKSolutions.psd1 -Force loads without errors
  • All 7 exported commands are present
  • Generate each report and verify HTML renders correctly
  • Verify PIM Audit Logs columns are properly constrained
  • Verify M365 License assignment type shows plain text
  • Verify Custom Security Attributes report discovers all sets
  • Confirm no XSS when display names contain <script> tags

Roy Klooster and others added 11 commits May 3, 2026 21:41
New-RKSolutionsReportTemplate provides the full HTML shell (head, CSS,
JS, brand elements) for all reports. Implements the editorial parchment
brand with light (chalk-white) and dark (warm parchment) themes,
Playfair Display/Source Serif 4/JetBrains Mono typography, breadcrumb
pill, eyebrow, stat tiles, status badges, and PS prompt footer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integrates the standalone Custom Security Attributes report into the
module. New public cmdlet with AttributeSet/AttributeNames/Filters
parameters, private helper for Graph data fetching and HTML generation
using the shared brand template. Adds CustomSecAttributeAssignment.Read.All
scope to Connect-RKGraph defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces ~920 lines of inline HTML/CSS/JS with a call to
New-RKSolutionsReportTemplate. All report-specific content (8 stat
tiles, 8 tabbed DataTables, filters) preserved as template parameters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces ~2050 lines of inline HTML/CSS/JS with a call to
New-RKSolutionsReportTemplate. Preserves shimmer animation and
group-jump highlight as report-specific CustomCss.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces ~3200 lines of inline HTML/CSS/JS with a call to
New-RKSolutionsReportTemplate. Also fixes duplicated function body
in Invoke-M365LicenseReportCore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…plate

Both New-AssignmentOverviewHtmlReport and New-DeviceVisualizationHtmlReport
now use New-RKSolutionsReportTemplate. Flow diagram and architecture
CSS preserved as report-specific CustomCss.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bump version to 1.1.0, correct PowerShellVersion to 7.0
- Add Get-CustomSecurityAttributesReport to FunctionsToExport
- Add CustomSecAttributeAssignment.Read.All to PERMISSIONS.md
- Add new cmdlet to consistency test parameter checks
- Update release notes with rebrand and new cmdlet info

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brighter, more energetic color palette while staying on-brand:
- Light: accent #c06828, gradient stat tiles, vivid badges, whiter bg
- Dark: accent #f0a850, gradient tiles, higher contrast text #f0e8d8,
  brighter badges (#60e068 green, #f0b840 amber, #f87068 red)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Card body gets explicit var(--bg-base) background
- Table and td inherit background from row instead of forcing white
- Even rows use var(--bg-elevated) in light, var(--bg-deep) in dark
- Override Bootstrap --bs-table-bg/striped/hover variables to transparent
- DataTables wrapper backgrounds set to transparent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tables in hidden panels (display:none) can't calculate their widths.
Now calls columns.adjust() after making a tab panel visible, so
Subscription Overview and Disabled Users tables match the first tab.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security (P0):
- XSS: HTML-encode all Graph data in all 5 HTML report generators
- XSS: fix JS filter dropdown injection with safe jQuery DOM API
- Validate $From as email/GUID in Send-EmailWithAttachment

Code quality (P1):
- Replace array += with List[PSObject].Add() in hot loops
- Add Write-Verbose to empty catch blocks in IntuneEnrollmentFlows
- Add Write-Warning when Graph paging hits 10k result cap
- Fix file deletion before email confirmed in IntuneAnomaliesReport
- Add null-check for undefined $mermaidDiagram variable

Maintenance (P2/P3):
- Sanitize tenant name in export path for IntuneAnomalies
- Fix typo: OperationSystemEdtionOverview -> OperatingSystemEditionOverview
- Wrap Invoke-Item in try/catch for cross-platform safety
- Use temp file instead of hardcoded C:\temp path
- Remove unused $Filters/$AttributeNames params
- Remove dead OS detection variables in Export-Results
- Initialize $emailSent before conditional block
- Translate Dutch comments to English
- Fix double-encoded HTML arrow entity in PIM audit logs

Report improvements:
- Rename New-RKSolutionsReportTemplate -> Get-RKSolutionsReportTemplate
- PIM Audit Logs: fix column widths with table-layout:fixed
- M365 License: replace assignment type badges with plain text
Copilot AI review requested due to automatic review settings May 8, 2026 08:25
@royklo royklo changed the title eat: shared report template, CSA report, and security hardening feat: shared report template, CSA report, and security hardening May 8, 2026
@royklo royklo merged commit a5c7b81 into main May 8, 2026
6 checks passed
@royklo royklo deleted the feature/report-rebrand-integration branch May 8, 2026 08:31
@royklo royklo restored the feature/report-rebrand-integration branch May 8, 2026 08:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces a shared HTML report template for the RKSolutions reporting module, adds a new Custom Security Attributes report cmdlet, and updates several reports/utilities to use the new template and improved safety checks.

Changes:

  • Added Get-RKSolutionsReportTemplate shared HTML shell and migrated report HTML generation to use it (reducing duplicated markup/JS/CSS).
  • Added Get-CustomSecurityAttributesReport (plus private data/HTML helpers) to report CSA across users, devices, and enterprise apps.
  • Updated Graph connection scopes/docs/tests and applied several safety/quality fixes (email sender validation, paging truncation warning, mermaid export guard, typo fix).

Reviewed changes

Copilot reviewed 15 out of 19 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
Tests/Consistency.Tests.ps1 Adds parameter contract coverage for the new CSA cmdlet.
module/RKSolutions.psm1 Loads the new shared template and CSA private helpers; exports the new public cmdlet.
module/RKSolutions.psd1 Bumps version to 1.1.0, updates description/release notes, exports new cmdlet.
module/Public/Get-IntuneEnrollmentFlowsReport.ps1 Avoids exporting empty Mermaid content.
module/Public/Get-IntuneAnomaliesReport.ps1 Fixes OS overview variable name and avoids deleting report before confirming email sent.
module/Public/Get-CustomSecurityAttributesReport.ps1 New public cmdlet wrapper for CSA report generation + optional email.
module/Public/Connect-RKGraph.ps1 Adds CSA-related scopes to the default scope union.
module/Private/Send-EmailWithAttachment.ps1 Adds validation for -From before constructing the Graph sendMail URI.
module/Private/Invoke-GraphRequestWithPaging.ps1 Warns when paging is truncated at the 10k cap.
module/Private/IntuneEnrollmentFlows.ps1 Migrates large HTML scaffolding to the shared template; updates UI/JS and diagram styling.
module/Private/Get-RKSolutionsReportTemplate.ps1 New shared HTML template (branding, theme toggle, DataTables helper).
module/Private/Export-Results.ps1 Removes dead OS detection logic.
module/Private/CustomSecurityAttributes.ps1 New CSA data retrieval and report HTML generation using the shared template.
docs/superpowers/specs/2026-05-03-report-integration-rebrand-design.md Adds design/spec documentation for rebrand + CSA integration.
docs/superpowers/plans/2026-05-03-report-rebrand-integration.md Adds an implementation plan document.
docs/PERMISSIONS.md Documents the new CSA permissions usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +95 to +98
if ($SendEmail -and (Test-Path -Path $htmlPath)) {
Remove-Item -Path $htmlPath -Force
Write-Host 'INFO: Temporary report file deleted.' -ForegroundColor Green
}
Write-Error "Invalid -From value '$From'. Must be an email address or user object ID."
return $false
}
$sendMailUri = "https://graph.microsoft.com/v1.0/users/$From/sendMail"
} while (-not $success -and $retryCount -lt $MaxRetries)
if (-not $success) { break }
if ($results.Count -gt 10000) { break }
if ($results.Count -gt 10000) {
if (`$.fn.DataTable && `$('#allAssignmentsTable').length > 0) {
var `$allTbl = `$('#allAssignmentsTable');
var preCategories = [], preTargets = [], preFilters = [];
function normFilter(s) { return String(s||'').replace(/\s*\((?:Include|Exclude)$$/i, '').trim() || String(s||''); }
@@ -2063,8 +1897,8 @@ function Get-ArchitectureDiagramFragment {
foreach ($gd in $DeviceGroupDetails) {
$encName = [System.Net.WebUtility]::HtmlEncode($gd.DisplayName)
$encType = [System.Net.WebUtility]::HtmlEncode($gd.GroupType)
$ruleCell = if ($gd.MembershipRule) { $gd.MembershipRule } else { "-" }
$groupTableRows += "<tr><td class=`"arch-group-cell`">$encName</td><td class=`"arch-group-type`">$encType</td><td class=`"arch-group-rule`">$ruleCell</td></tr>"
$ruleCell = if ($gd.MembershipRule -and $gd.MembershipRule -ne "-") { '<code>' + $gd.MembershipRule + '</code>' } else { "-" }

# ===== Build Overview panel =====
$overviewHeaders = " <th>Type</th>`n <th>Name</th>"
foreach ($s in $sets) { $overviewHeaders += "`n <th>$s</th>" }
Comment on lines +347 to +349
$uniqueVals = @($allEntitiesForSet | ForEach-Object { $_.$a } | Where-Object { $_ -and $_ -ne '-' } | Sort-Object -Unique)
$options = ($uniqueVals | ForEach-Object { "<option value=`"$([System.Net.WebUtility]::HtmlEncode($_))`">$([System.Net.WebUtility]::HtmlEncode($_))</option>" }) -join ''
$filterDropdowns += "`n <select class=`"form-select ${filterId}-filter`" style=`"max-width:180px;`"><option value=`"`">All $a</option>$options</select>"
Comment on lines +35 to +36
'CustomSecAttributeAssignment.Read.All',
'CustomSecAttributeDefinition.Read.All'
Comment thread docs/PERMISSIONS.md
Comment on lines 11 to 16
| Permission | Used by |
|------------|---------|
| **AuditLog.Read.All** | Get-EntraAdminRolesReport, Get-M365LicenseAssignmentReport |
| **CustomSecAttributeAssignment.Read.All** | Get-CustomSecurityAttributesReport |
| **CloudLicensing.Read** | Get-M365LicenseAssignmentReport |
| **CloudPC.Read.All** | Get-IntuneEnrollmentFlowsReport, Get-IntuneAnomaliesReport |
Comment thread module/RKSolutions.psd1
LicenseUri = 'https://opensource.org/licenses/MIT'
ProjectUri = 'https://github.com/royklo/RKSolutions-Module'
ReleaseNotes = @'
1.1.0 - Rebranded all HTML reports with Carbon Ember design (Geist/Geist Mono typography, pill-style tabs, neutral dark theme). Added Get-CustomSecurityAttributesReport with multi-entity support (users, devices, enterprise apps), dynamic attribute set discovery, and coverage metrics. Fixed XSS vulnerabilities in HTML output. Added equal-width tabs, dark mode pagination, and locked DataTable column widths. Added skuFamily for OS edition detection, Windows 11 25H2 support. Added CustomSecAttributeAssignment.Read.All and CustomSecAttributeDefinition.Read.All scopes.
royklo pushed a commit that referenced this pull request Jun 17, 2026
…al devices

Resolve-IntuneLapsAnomalies read \$d.OwnerType but Get-AllDeviceData emits
DeviceOwnership. Every real device hit the LAPS tab with an empty Ownership
cell. The initial smoke test passed because it synthesized OwnerType directly
on the fake device - the test was masking the bug instead of catching it.

Fix:
- Read \$d.DeviceOwnership in Resolve-IntuneLapsAnomalies.
- Switch all three resolver output literals to [PSCustomObject][ordered]@{}
  so property iteration order is deterministic. While here, align literal
  order with the HTML <td> column order (Severity before Status) - the
  report rendered correctly via property-name access but Export-Csv on the
  same objects would have emitted them in the wrong order.
- Update test fixture to use the real Get-AllDeviceData field name
  (DeviceOwnership) AND assert the materialised value, not just the column
  presence, so a future field-name drift can't sneak past the test the way
  it did this time.

Fixes copilot findings #1-#5 on the PR. Finding #6 (-UseBasicParsing breaks
PS 7+) is rejected as empirically wrong: the parameter is a documented no-op
on PS 6+, verified working on PS 7.5.4, and the settings catalog has been
downloading successfully all session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants