Public/Get-VerkadaAccessUserReport.ps1
function Get-VerkadaAccessUserReport{ <# .SYNOPSIS Returns a report of all doors a user has access to and by what means. .DESCRIPTION This function will return all the doors the user/s have access to, the credentials assigned to the user, the last time they accessed a door, and their group membership. This function requires that a valid Verkada Access User object be submitted. The org_id and reqired tokens can be directly submitted as parameters, but is much easier to use Connect-Verkada to cache this information ahead of time and for subsequent commands. .LINK https://github.com/bepsoccer/verkadaModule/blob/master/docs/function-documentation/Get-VerkadaAccessUserReport.md .EXAMPLE Get-VerkadaAccessUserViaGraphql -userId 'c1cb427f-9ef4-4800-95ec-4a580bfa2bf1' | Get-VerkadaAccessUserReport This will get the Acces user object for userId c1cb427f-9ef4-4800-95ec-4a580bfa2bf1 and return the access report for that user. The org_id and tokens will be populated from the cached created by Connect-Verkada. .EXAMPLE Get-VerkadaAccessUserViaGraphql -userId 'c1cb427f-9ef4-4800-95ec-4a580bfa2bf1' | Get-VerkadaAccessUserReport -beautify | Export-Csv ~/Desktop.ACusersReport.csv -NoTypeInformation This will get the Acces user object for userId c1cb427f-9ef4-4800-95ec-4a580bfa2bf1 and return the access report for that user in a consumeable way for a csv report. The org_id and tokens will be populated from the cached created by Connect-Verkada. .EXAMPLE Get-VerkadaAccessUserViaGraphql -userId 'c1cb427f-9ef4-4800-95ec-4a580bfa2bf1' | Get-VerkadaAccessUserReport -outReport This will get the Acces user object for userId c1cb427f-9ef4-4800-95ec-4a580bfa2bf1 and return the access report for that user in a pretty HTML file. The org_id and tokens will be populated from the cached created by Connect-Verkada. .EXAMPLE Read-VerkadaAccessUsers | Get-VerkadaAccessUserReport -org_id '7cd47706-f51b-4419-8675-3b9f0ce7c12d' -x_verkada_token 'a366ef47-2c20-4d35-a90a-10fd2aee113a' -x_verkada_auth 'auth-token-uuid-dscsdc' -usr 'a099bfe6-34ff-4976-9d53-ac68342d2b60' This will get all the Acces user objects in an organization and return the access report for that user. The org_id and tokens are submitted as parameters in the call. #> [CmdletBinding(PositionalBinding = $true)] param ( #The Access user object the report will run against [Parameter(ValueFromPipeline, Position = 0)] [ValidateScript( { $requiredProperties=@("accessCards","accessCardsRaw","accessGroups","accessUserRoles","bluetoothAccess","companyName","created","deactivated","deleted","department","departmentId","email","emailVerified","employeeId","employeeTitle","employeeType","firstName","lastActiveAccess","lastLogin","lastName","middleName","mobileAccess","modified","name","organizationId","phone","provisioned","roleGrant","userCodes","userCodesRaw","userId","__typename") $members=Get-Member -InputObject $_ -MemberType NoteProperty $missingProperties=Compare-Object -ReferenceObject $requiredProperties -DifferenceObject $members.Name -PassThru -ErrorAction SilentlyContinue if (-not($missingProperties)){ $true } else{ $missingProperties | ForEach-Object { Throw [System.Management.Automation.ValidationMetadataException] "Property: '$_' missing" } } } )] [Object]$user, #The UUID of the organization the user belongs to [Parameter()] [ValidateNotNullOrEmpty()] [ValidatePattern('^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$')] [String]$org_id = $Global:verkadaConnection.org_id, #The Verkada(CSRF) token of the user running the command [Parameter()] [ValidateNotNullOrEmpty()] [ValidatePattern('^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$')] [string]$x_verkada_token = $Global:verkadaConnection.csrfToken, #The Verkada Auth(session auth) token of the user running the command [Parameter()] [ValidateNotNullOrEmpty()] [string]$x_verkada_auth = $Global:verkadaConnection.userToken, #The UUID of the user account making the request [Parameter()] [ValidateNotNullOrEmpty()] [ValidatePattern('^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$')] [string]$usr = $Global:verkadaConnection.usr, #This is a switch to indicate we're gonna try to make the report prettier [Parameter()] [switch]$beautify, #This is a switch to indicate we're gonna try to make the report a pretty html [Parameter()] [switch]$outReport, #This is the path the pretty html report will attempt to be saved to [Parameter()] [string]$reportPath, #Number of threads allowed to multi-thread the task [Parameter()] [ValidateRange(1,20)] [int]$threads=10 ) begin { #parameter validation if ([string]::IsNullOrEmpty($org_id)) {throw "org_id is missing but is required!"} if ([string]::IsNullOrEmpty($x_verkada_token)) {throw "x_verkada_token is missing but is required!"} if ([string]::IsNullOrEmpty($x_verkada_auth)) {throw "x_verkada_auth is missing but is required!"} if ([string]::IsNullOrEmpty($usr)) {throw "usr is missing but is required!"} $accessLevels = Get-VerkadaAccessLevels -org_id $org_id -x_verkada_token $x_verkada_token -x_verkada_auth $x_verkada_auth -usr $usr $allDoors = Get-VerkadaAccessDoors -org_id $org_id -x_verkada_token $x_verkada_token -x_verkada_auth $x_verkada_auth -usr $usr $accessSites = Get-VerkadaAccessSite -org_id $org_id -x_verkada_token $x_verkada_token -x_verkada_auth $x_verkada_auth -usr $usr $outUsers = @() if($beautify.IsPresent){$beautify = $true} else {$beautify = $false} if($outReport.IsPresent){$outReport = $true} else {$outReport = $false} $helpers = { #some helper functions function prettyGrouping { param ( $myInput, $groupProperty1, $groupProperty2, $groupProperty3 ) $temp = @() $myInput | Group-Object -Property $groupProperty1 | ForEach-Object {$ob = @{"$($_.Name.toString())"=$_.Group;};$ob = $ob | ConvertTo-Json -Depth 10 | ConvertFrom-Json; $temp += $ob} $temp = groupRemoveProp $temp $groupProperty1 foreach ($group in $temp){ $temp2 = @() $group.($group.psobject.Properties.name) | Group-Object -Property $groupProperty2 | ForEach-Object {$ob = @{"$($_.Name.toString())"=$_.Group;};$ob = $ob | ConvertTo-Json -Depth 10 | ConvertFrom-Json; $temp2 += $ob} $temp2 = groupRemoveProp $temp2 $groupProperty2 foreach ($group2 in $temp2){ $temp3 = @() $group2.($group2.psobject.Properties.name) | Group-Object -Property $groupProperty3 | ForEach-Object {$ob = @{"$($_.Name.toString())"=$_.Group;};$ob = $ob | ConvertTo-Json -Depth 10 | ConvertFrom-Json; $temp3 += $ob} $temp3 = groupRemoveProp $temp3 $groupProperty3 foreach ($name in $temp3){ $names = $name.($name.psobject.Properties.name) | Select-Object -ExpandProperty name $name.($name.psobject.Properties.name) = $names } $group2.($group2.psobject.Properties.name) = $temp3 } $group.($group.psobject.Properties.name) = $temp2 } return $temp | ConvertTo-Json -Depth 10 -Compress } function groupRemoveProp { param ( $myInput, $groupingProp ) foreach ($group in $myInput){ $thing = $group.($group.psobject.Properties.name) foreach ($prop in $thing){ $prop.PSObject.Properties.Remove($groupingProp) } } return $myInput } function listFormatter { param ( $myInput, $level=1, $arrayItem, $open, $close ) switch ($level) { default {} 1 {$class = 'group'} 2 {$class = 'site'} 3 {$class = 'sched'} 4 {$class = 'door'} } $s='' if ($myInput.GetType().baseType.name -eq 'Array'){ for($i=0; $i -lt $myInput.count; $i++) { $close=$false $open=$false if($i -eq $($($myInput.count)-1)){ $close = $true } elseif ($i -eq 0) { $open = $true } listFormatter $myInput[$i] $level $true $open $close } } elseif ($myInput.GetType().name -eq 'String') { if (!($arrayItem) -or $open){ $s+="<ul>" } $s+="<li class=`"$class`">$myInput</li>" if (!($arrayItem) -or $close){ $s+="</ul></ul></ul>" } $s } else { if($level -eq 1){ $s+="<li class=`"$class`">$($myInput.psobject.Properties.name)</li>" } else { $s+="<ul><li class=`"$class`">$($myInput.psobject.Properties.name)</li>" } $s $level++ listFormatter $myInput.($myInput.psobject.Properties.name) $level } } function jsonToList { param ( $myJson ) $json = $myJson | ConvertFrom-Json $s='<ul>' $s += (listFormatter $Json) $s+="</ul>" return $s } } $jobs = @() $vMod = Get-Module verkadaModule | Select-Object -ExpandProperty Path } #end begin process { $jobs += Start-ThreadJob -InitializationScript $helpers -ThrottleLimit $threads -ScriptBlock { Import-Module $using:vMod $user = $using:user | Select-Object userId,name,email,@{name='accessGroups';expression={$_.accessGroups.group}},accessCards,bluetoothAccess,mobileAccess,@{name='lastActiveAccess';expression={Get-Date -UnixTimeSeconds $_.lastActiveAccess}} $userDoors = @() $accessGroups = @() foreach ($group in $user.accessGroups){ #find which access levels that group is a part of $acLevels =@() $acLevels += $using:accessLevels | Where-Object {$_.userGroups -contains $group.userGroupId} $accessGroups += $group.name #find which doors that access level has access to and schedule foreach ($level in $acLevels){ $schedule = $level | Select-Object -ExpandProperty events $schedule = Get-HumanReadableSchedule $schedule $doors = $level | Select-Object -ExpandProperty doors foreach ($d in $doors){ $ds = $using:allDoors | Where-Object {$_.doorId -eq $d} | Select-Object name,doorId,accessControllerId $ds | Add-Member -NotePropertyName 'siteName' -NotePropertyValue ($using:accessSites | Where-Object {$_.accessControllers -contains $ds.accessControllerId} | Select-Object -ExpandProperty name) $ds | Add-Member -NotePropertyName 'schedule' -NotePropertyValue $schedule $ds | Add-Member -NotePropertyName 'group' -NotePropertyValue $group.name $ds.PSObject.Properties.Remove('accessControllerId') $ds.PSObject.Properties.Remove('doorId') $userDoors += $ds } } } $user.accessGroups = $accessGroups if ($using:beautify -or $using:outReport){ $userDoors = prettyGrouping $userDoors 'group' 'siteName' 'schedule' $user.accessGroups = $user.accessGroups | ConvertTo-Json #-Compress if ($user.accessCards){ try { #retrieve access cards $creds = Get-VerkadaAccessCredential -userId $user.userId -org_id $using:org_id -x_verkada_token $using:x_verkada_token -x_verkada_auth $using:x_verkada_auth -usr $using:usr $user.accessCards = $creds.accessCards | Select-Object active,cardType,cardParams,@{name='lastUsed';expression={Get-Date -UnixTimeSeconds $_.lastUsed}} | ConvertTo-Json #-Compress } catch { } } if($using:outReport){ if($userDoors){ $userDoors = jsonToList $userDoors } $user.PSObject.Properties.Remove('userId') } } $user | Add-Member -NotePropertyName 'doors' -NotePropertyValue $userDoors $user } #$outUsers += $user } #end process end { $outUsers = $jobs | Receive-Job -AutoRemoveJob -Wait -WarningVariable +w -ErrorVariable +e foreach ($line in $w){Write-Output "Warning: $line"} foreach ($line in $e){Write-Output "Error: $line"} Remove-Variable -Name w -ErrorAction SilentlyContinue Remove-Variable -Name e -ErrorAction SilentlyContinue #$outUsers = $jobs $outUsers if ($outReport){ function testReportPath { param () $filePath = Read-Host -Prompt 'Please provide the path for the AC report to be saved to.' try { Get-ChildItem -Path $filePath -ErrorAction Stop | Out-Null return $filePath } catch { Write-Warning $_.Exception.Message testReportPath } } if(!([string]::IsNullOrEmpty($reportPath))){ try { Get-ChildItem -Path $reportPath -ErrorAction Stop | Out-Null $reportFile = $reportPath } catch { Write-Warning $_.Exception.Message testReportPath } } else { $reportFile = testReportPath } $reportFile = (Get-Item $reportFile).fullName + "/ACuserReport-$(Get-Date -Format MMddyyyy).html" $Head = @" <style> body { font-family: "Arial"; font-size: 11pt; color: #4C607B; background-color: #F5F5F5; margin-top: 50px; } table { border-collapse: collapse; } th, td { max-width: 550px; } th { font-size: 12pt; text-align: left; background-color: #F1F1F1; color: #949BA0; font-weight: bold; position: sticky; top: 0; } td { color: #949BA0; vertical-align: top; padding-top: 10px; } td:first-child { width: 300px; color: #000000; } td:nth-child(3), td:nth-child(4), td:nth-child(5), td:nth-child(6) { padding: 10px; } td:nth-child(4), td:nth-child(5){ width: 100px; } tr:hover td { Background-Color: #F5FBFE; Color: #51B9ED; } tr:nth-child(odd) { Background-Color: #F8F8F8; } tr:nth-child(even) { Background-Color: #FFFFFF; } pre { white-space: pre-wrap; } #myInput { background-image: url('data:image/svg+xml;utf8,<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26" height="26" viewBox="0 0 26 26"><g fill="none" fill-rule="evenodd"><path d="M11.65 20.21c-4.78 0-8.65-3.77-8.65-8.4s3.87-8.4 8.65-8.4a8.53 8.53 0 0 1 8.64 8.4c0 4.63-3.86 8.4-8.64 8.4z" stroke="%23949BA0" stroke-width="2"></path><path d="M17.34 17.96a1.43 1.43 0 0 1 1.98 0l3.7 3.64a1.35 1.35 0 0 1 0 1.93l.06-.05a1.43 1.43 0 0 1-1.99 0l-3.7-3.63a1.35 1.35 0 0 1 0-1.93l-.06.04z" fill="%23949BA0"></path></g></svg>'); background-position: 10px 10px; background-repeat: no-repeat; width: 375px; padding: 12px 20px 12px 40px; border: none; margin-bottom: 12px; Background-Color: #FFFFFF; font-family: "Arial"; font-size: 11pt; color: #4C607B; } ::placeholder { color: #949BA0; font-family: "Arial"; font-size: 11pt; } #myInput:focus{ font-family: "Arial"; font-size: 11pt; color: #4C607B; border: none; outline: none; } h2 { color: #949BA0; } li.group { list-style-image: url('data:image/svg+xml;utf8,<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14" viewBox="0 0 26 26"><g fill-rule="evenodd" fill="%23949BA0"><path d="M17.07 3.6c3.25 0 5.11 1.92 4.64 6.24-.45 4.16-2.52 4.3-2.27 5.27.23.9 5.7.13 6.45 4.16.06.3 0 .97-.86.97h-5.51c-.65-2.17-2.29-3.47-4.54-4.17a12.12 12.12 0 0 0-.52-.15c.54-1.03.9-2.25 1.07-3.76.35-3.2-.43-5.65-2.13-7.12.81-.98 2.07-1.44 3.67-1.44z"></path><path d="M1.05 22.32c-.98 0-1.05-.71-.98-1.02.78-3.98 6.21-3.21 6.44-4.1.25-.98-1.82-1.12-2.27-5.28-.47-4.32 1.39-6.24 4.64-6.24S14 7.6 13.53 11.92c-.45 4.16-2.53 4.3-2.28 5.27.24.9 5.7.12 6.46 4.16.05.3 0 .97-.87.97H1.05z" fill-rule="nonzero"></path></g></svg>'); } li.site { list-style-image: url('data:image/svg+xml;utf8,<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="18" viewBox="0 0 24 24" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.09 11.09C3.38 6.36 7.07 2 12 2s8.62 4.36 7.91 9.09c-.8 5.45-6.36 10.07-7.64 10.84-.08.05-.17.07-.27.07s-.19-.02-.27-.07c-1.27-.77-6.83-5.38-7.64-10.85zM12 20.91c1.47-1.05 6.21-5.16 6.92-9.98a6.87 6.87 0 0 0-1.6-5.5A7.03 7.03 0 0 0 12 3.01a7 7 0 0 0-5.32 2.43 6.87 6.87 0 0 0-1.6 5.5c.71 4.85 5.49 8.97 6.92 9.98zM8 10a4 4 0 1 1 8 0 4 4 0 1 1-8 0zm1 0a3.01 3.01 0 0 0 3 3c1.65 0 3-1.34 3-3a3.01 3.01 0 0 0-3-3 3 3 0 0 0-3 3z" fill="%23949BA0"></path></svg>'); } li.sched { list-style-image: url('data:image/svg+xml;utf8,<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="18" viewBox="0 0 24 24" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M20 4h-3V2h-1v2H8V2H7v2H4a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm-4 3H8V5h8v2zM3 6a1 1 0 0 1 1-1h3v2H3V6zm18 14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8h18v12zm0-13h-4V5h3a1 1 0 0 1 1 1v1zm-7 4h-3v1h3v-1zm-3 3h3v1h-3v-1zm5 0h3v1h-3v-1zm0-3h3v1h-3v-1zm3 6h-3v1h3v-1zm-8 0h3v1h-3v-1zm-6 2v-9h4v9H5zm1-8v7h2v-7H6z" fill="%23949BA0"></path></svg>'); } li.door{ list-style-image: url('data:image/svg+xml;utf8,<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="11" height="16" viewBox="0 0 21 31" fill="none"><path d="M13.54 1.22L1.34 2.98a.4.4 0 0 0-.34.4v24.9a.4.4 0 0 0 .36.4l12.2 1.18a.4.4 0 0 0 .44-.4V1.61a.4.4 0 0 0-.46-.39z" stroke="%23949BA0" stroke-width="2"></path><path d="M17.6 3.37v-1 1zm0 24.68v-1 1zm0-25.68H14v2h3.6v-2zm-3.59 26.68h3.6v-2H14v2zm6-2.4V4.56h-2v22.09h2zm-2.4 2.4c.32 0 .65 0 .91-.03s.68-.1 1.02-.44c.33-.33.4-.75.44-1.01.03-.27.02-.6.02-.92h-2v.44a3 3 0 0 1-.01.26c-.02.13-.03-.02.13-.18s.32-.15.18-.14a3.14 3.14 0 0 1-.25.02h-.45v2zm0-24.68h.44l.27.01c.06.01.07.02.05 0a.63.63 0 0 1-.36-.33L17.99 4l.01.2v.36h2c0-.27 0-.57-.03-.82a1.5 1.5 0 0 0-.5-.98 1.7 1.7 0 0 0-.96-.37c-.26-.02-.58-.02-.9-.02v2z" fill="%23949BA0"></path><path d="M9.29 18.02a1.44 1.44 0 0 1-1.43-1.44c0-.8.64-1.44 1.43-1.44.79 0 1.43.64 1.43 1.44 0 .8-.65 1.44-1.43 1.44z" fill="%23949BA0"></path></svg>'); } .main { margin: auto; } </style> "@ $preContent = @" <svg width="450" height="100" viewBox="0 0 90 20" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.17 13.38L19.22 3.33c.23-.22.58-.22.78 0l3.29 3.27a.53.53 0 0 1 0 .78l-9.66 9.68c-.23.23-.58.23-.78 0l-3.68-3.68zM3.2 7.4c-.23-.23-.23-.57 0-.78l3.27-3.27a.53.53 0 0 1 .77 0l5.09 5.09-4.05 4.05-5.08-5.1zM32.49 5h-2l4.08 10.81h2.17l4.08-10.8h-2l-3.17 8.41-3.16-8.41zm11.48 10.98c2.16 0 3.16-1.31 3.16-1.31l-1.02-1.25s-.77.94-2.08.94c-1.25 0-1.98-.75-2.17-1.61h5.64s.08-.39.08-.86c0-2.06-1.49-3.74-3.61-3.78A3.96 3.96 0 0 0 40 11.36a3.97 3.97 0 0 0 3.97 4.62zM43.8 9.8c1 0 1.64.61 1.86 1.47h-3.78c.23-.86.84-1.47 1.92-1.47zm5.23-1.56h1.78v1.09h.08s.78-1.23 2.17-1.23h.3v1.92s-.22-.08-.6-.08c-1.09 0-1.93.86-1.93 2.17v3.7h-1.77l-.02-7.57zm7.22-3.24h-1.78l-.02 10.8h1.78v-1.86l1.16-1.3 2.17 3.16h2l-3-4.39 2.86-3.17h-2.17l-3 3.31V5.01zm10.58 9.8h-.08s-.7 1.17-2.25 1.17c-1.47 0-2.47-1-2.47-2.17 0-1.22.92-2.16 2.25-2.39l2.55-.47c0-.53-.61-1.16-1.47-1.16-1.12 0-1.86.92-1.86.92l-1.08-1.09s1.08-1.53 3-1.53c1.86 0 3.17 1.35 3.17 3v4.7H66.8l.02-.98zm0-2.47l-1.78.3c-.92.17-1.22.48-1.22.93 0 .45.47.92 1.16.92 1 0 1.86-.86 1.86-1.92l-.02-.23zm8.85 2.4h.08v1.08h1.78V5h-1.78V9.3h-.08s-.7-1.23-2.48-1.23c-1.77 0-3.39 1.62-3.39 3.95 0 2.32 1.62 3.94 3.4 3.94 1.77 0 2.47-1.23 2.47-1.23zm-2-4.87a2.1 2.1 0 0 1 2.08 2.17c0 1.24-.92 2.16-2.09 2.16a2.1 2.1 0 0 1-2.08-2.16c0-1.25.92-2.17 2.08-2.17zm9.86 4.95h-.08s-.7 1.16-2.25 1.16c-1.47 0-2.47-1-2.47-2.17 0-1.22.92-2.16 2.25-2.39l2.55-.47c0-.53-.61-1.16-1.47-1.16-1.12 0-1.86.92-1.86.92l-1.08-1.09s1.08-1.53 3-1.53c1.86 0 3.17 1.35 3.17 3v4.7h-1.78l.02-.98zm0-2.48l-1.77.3c-.92.17-1.23.48-1.23.93 0 .45.47.92 1.17.92 1 0 1.85-.86 1.85-1.92l-.02-.23z" fill="#030E16"></path></svg> <div class="main"> <input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names.." title="Type in a name"> "@ $postContent = @" </div> <script> function myFunction() { var input, filter, table, tr, td, i, txtValue; input = document.getElementById("myInput"); filter = input.value.toUpperCase(); table = document.getElementById("report"); tr = table.getElementsByTagName("tr"); for (i = 0; i < tr.length; i++) { td = tr[i].getElementsByTagName("td")[0]; if (td) { txtValue = td.textContent || td.innerText; if (txtValue.toUpperCase().indexOf(filter) > -1) { tr[i].style.display = ""; } else { tr[i].style.display = "none"; } } } } </script> "@ $jsonProps = @('accessGroups','accessCards') foreach ($prop in $jsonProps){ $outUsers | ForEach-Object { if(!([string]::IsNullOrEmpty($_.($prop)))){ $_.($prop) = "<pre>$($_.($prop))</pre>" } } } $outUsers | ConvertTo-Html -Property @{name='Name';expression={$_.name}},@{name='Email';expression={$_.email}},@{name='Doors';expression={$_.doors}},@{name='Bluetooth';expression={$_.bluetoothAccess}},@{name='Mobile';expression={$_.mobileAccess}},@{name='Creds';expression={$_.accessCards}},@{name='Groups';expression={$_.accessGroups}} -Title 'AC User Report' -Head $Head -PreContent $preContent -PostContent $postContent | foreach-object {[System.Web.HttpUtility]::HtmlDecode($_)} | ForEach-Object {$_.Replace('<table>','<table id="report">')} | Set-Content $reportFile } } #end end } #end function |