<# .SYNOPSIS Parse Microsoft Graph permissions reference from raw Markdown and write JSON. .DESCRIPTION Downloads: https://raw.githubusercontent.com/microsoftgraph/microsoft-graph-docs-contrib/main/concepts/permissions-reference.md Produces: microsoft-graph-permissions.json (array of objects with Scope, ApplicationDisplayText, ApplicationDescription, DelegatedDisplayText, DelegatedDescription) PowerShell 5.1 compatible. No external dependencies. #> param( [string]$MarkdownUrl = 'https://raw.githubusercontent.com/microsoftgraph/microsoft-graph-docs-contrib/main/concepts/permissions-reference.md', [string]$OutFile = "$PSScriptRoot\microsoft-graph-permissions-metadata.json" ) function Write-ErrorAndExit($msg, $code=1) { Write-Error $msg exit $code } Write-Host "Downloading markdown from $MarkdownUrl ..." try { $resp = Invoke-WebRequest -Uri $MarkdownUrl -ErrorAction Stop $md = $resp.Content } catch { Write-ErrorAndExit "Failed to download markdown: $_" } if (-not $md) { Write-ErrorAndExit "Empty content received from $MarkdownUrl" } # Split into lines $lines = $md -split "`n" # Helper to trim and normalize a markdown table cell value function NormalizeCell($s) { if ($null -eq $s) { return '' } $v = $s.Trim() # remove leading/trailing backticks or code fences if present and normalize whitespace $v = $v -replace '^\s*`+','' -replace '`+\s*$','' $v = $v -replace '\s+', ' ' return $v.Trim() } $results = @() # Iterate through lines, find "### " headings and parse table until '---' separator or next '###' for ($i = 0; $i -lt $lines.Count; $i++) { $line = $lines[$i].TrimEnd("`r") # normalize CRLF if ($line.StartsWith('### ')) { # Extract scope text after '### ' (some headings may have spaces; keep full remainder) $scope = $line.Substring(4).Trim() if ($scope -eq '') { continue } # Prepare structure for this scope $current = @{ Scope = $scope # store intermediate label data as nested hashtable for easy appends _labels = @{} } # Advance to lines after heading $j = $i + 1 # Collect table lines until the markdown '---' separator or next "### " or until a blank line that signals end of section $tableLines = @() while ($j -lt $lines.Count) { $ln = $lines[$j].TrimEnd("`r") if ($ln -match '^\s*---\s*$') { break } # markdown horizontal rule separator between entries if ($ln.Trim().StartsWith('### ')) { break } # next heading # Only care about lines that begin with '|' (markdown table rows) OR continuation lines that also start with '|' if ($ln.Trim().StartsWith('|')) { $tableLines += $ln } $j++ } # Move outer index forward to skip parsed block $i = $j - 1 # If no table lines found, add empty object and continue if ($tableLines.Count -eq 0) { $results += [PSCustomObject]@{ Scope = $scope ApplicationDisplayText = '' ApplicationDescription = '' DelegatedDisplayText = '' DelegatedDescription = '' } continue } # Parse table lines: handle normal rows and continuation lines # We'll keep track of the last label processed so continuation lines append to it. $lastLabel = $null for ($k = 0; $k -lt $tableLines.Count; $k++) { $tline = $tableLines[$k].Trim() # split by '|' — leading/trailing pipes produce empty elements; remove them # Use -split with regex to preserve empty middle cells $partsRaw = $tline -split '\|' # Trim parts and remove first/last if they are empty (from leading/trailing '|') $parts = @() foreach ($p in $partsRaw) { $parts += $p.Trim() } # Remove leading empty element if present if ($parts.Count -gt 0 -and $parts[0] -eq '') { $parts = $parts[1..($parts.Count-1)] } # Remove trailing empty element if present if ($parts.Count -gt 0 -and $parts[-1] -eq '') { $parts = $parts[0..($parts.Count-2)] } # After trimming, if no meaningful parts, skip if ($parts.Count -eq 0) { continue } # If the first column looks like a known row label, treat as new row $labelCandidate = $parts[0] $knownLabels = @('Identifier','DisplayText','Description','AdminConsentRequired','Category') if ($knownLabels -contains $labelCandidate) { # New labeled row $lastLabel = $labelCandidate # Application column is the 2nd part if present $appCell = '' $delCell = '' if ($parts.Count -ge 2) { $appCell = NormalizeCell($parts[1]) } if ($parts.Count -ge 3) { $delCell = NormalizeCell($parts[2]) } # Initialize or set the label entry in current._labels as a hashtable with App and Del $labObj = @{ App = $appCell Del = $delCell } $current._labels[$labelCandidate] = $labObj # Continue to next line (note: some Description or DisplayText rows might be followed by continuation lines) continue } else { # This is a continuation line (e.g. "| Allows the app to ...") # After trimming 'parts', typically parts.Count == 1 and parts[0] contains the continuation text. $contText = ($parts -join ' | ') # join in case there were internal pipes (rare) $contText = NormalizeCell($contText) if ($lastLabel) { # Append continuation text to the next empty cell of the lastLabel, # preferring to fill Del if App is already present, otherwise App. $entry = $current._labels[$lastLabel] if (-not $entry) { $entry = @{ App = ''; Del = '' } $current._labels[$lastLabel] = $entry } # Decide whether continuation belongs to App or Del: # If App is empty -> append to App; else if Del is empty -> append to Del; # else append to Del (multiple continuation lines). if ([string]::IsNullOrWhiteSpace($entry.App)) { if ($entry.App) { $entry.App += ' ' + $contText } else { $entry.App = $contText } } elseif ([string]::IsNullOrWhiteSpace($entry.Del)) { if ($entry.Del) { $entry.Del += ' ' + $contText } else { $entry.Del = $contText } } else { # both filled: append to Del separated by space $entry.Del = ($entry.Del + ' ' + $contText).Trim() } } else { # continuation but no last label — ignore } } } # end for each table line # Map parsed label data into the final output fields, using empty fallback when missing $dispApp = '' $dispDel = '' $descApp = '' $descDel = '' if ($current._labels.ContainsKey('DisplayText')) { $dt = $current._labels['DisplayText'] $dispApp = if ($dt.App) { $dt.App } else { '' } $dispDel = if ($dt.Del) { $dt.Del } else { '' } } if ($current._labels.ContainsKey('Description')) { $dd = $current._labels['Description'] $descApp = if ($dd.App) { $dd.App } else { '' } $descDel = if ($dd.Del) { $dd.Del } else { '' } } if ($current._labels.ContainsKey('AdminConsentRequired')) { $dd = $current._labels['AdminConsentRequired'] $acrApp = if ($dd.App) { $dd.App } else { '' } $acrDel = if ($dd.Del) { $dd.Del } else { '' } } if ($current._labels.ContainsKey('Identifier')) { $dd = $current._labels['Identifier'] $idnApp = if ($dd.App) { $dd.App } else { '' } $idnDel = if ($dd.Del) { $dd.Del } else { '' } } # Add results object $results += [PSCustomObject]@{ Scope = $scope ApplicationDisplayText = $dispApp ApplicationDescription = $descApp ApplicationAdminConsentRequired = ("$acrApp".Trim().ToLowerInvariant() -eq "yes") ApplicationIdentifier = $idnApp DelegatedDisplayText = $dispDel DelegatedDescription = $descDel DelegatedAdminConsentRequired = ("$acrDel".Trim().ToLowerInvariant() -eq "yes") DelegatedIdentifier = $idnDel } } } # Re-shaped results $dictionaryResult = [ordered]@{}; foreach ($item in $results) { $entry = [ordered]@{} # write application stuff if ([string]::IsNullOrWhiteSpace($item.ApplicationDisplayText) -or "$($item.ApplicationDisplayText)".Trim() -eq "-") { $entry["Application"] = $null; } else { $entry["Application"] = [PSCustomObject]@{ Identifier = $item.ApplicationIdentifier DisplayText = $item.ApplicationDisplayText Description = $item.ApplicationDescription AdminConsentRequired = $item.ApplicationAdminConsentRequired } } # write delegated stuff if ([string]::IsNullOrWhiteSpace($item.DelegatedDisplayText) -or "$($item.DelegatedDisplayText)".Trim() -eq "-") { $entry["Delegated"] = $null; } else { $entry["Delegated"] = [PSCustomObject]@{ Identifier = $item.DelegatedIdentifier DisplayText = $item.DelegatedDisplayText Description = $item.DelegatedDescription AdminConsentRequired = $item.DelegatedAdminConsentRequired } } $dictionaryResult[$item.Scope] = $entry; } # Formats JSON in a nicer format than the built-in ConvertTo-Json does. # Source: https://github.com/PowerShell/PowerShell/issues/2736#issue-190538839 function Format-Json([Parameter(Mandatory, ValueFromPipeline)][String] $json) { $indent = 0; ($json -Split '\n' | % { if ($_ -match '[\}\]]') { # This line contains ] or }, decrement the indentation level $indent-- } $line = (' ' * $indent * 2) + $_.TrimStart().Replace(': ', ': ') if ($_ -match '[\{\[]') { # This line contains [ or {, increment the indentation level $indent++ } $line }) -Join "`n" } # Write JSON Write-Host "Parsed $($results.Count) permission entries. Writing JSON to $OutFile ..." #$results | ConvertTo-Json -Depth 6 | Out-File -FilePath $OutFile -Encoding UTF8 $dictionaryResult | ConvertTo-Json -Depth 6 | Format-Json | Out-File -FilePath $OutFile -Encoding UTF8 Write-Host "Done." # Example: show first 5 entries $dictionaryResult | Select-Object -First 5 | Format-Table -AutoSize