public/Invoke-VPASMetricsPSM.ps1
<#
.Synopsis RUN VARIOUS PSM METRICS FROM CYBERARK CREATED BY: Vadim Melamed, EMAIL: vpasmodule@gmail.com .DESCRIPTION USE THIS FUNCTION TO GENERATE VARIOUS PSM RELATED METRICS FROM CYBERARK .LINK https://vpasmodule.com/commands/Invoke-VPASMetricsPSM .NOTES SelfHosted: TRUE PrivCloudStandard: TRUE SharedServices: TRUE .PARAMETER token HashTable of data containing various pieces of login information (PVWA, LoginToken, HeaderType, etc). If -token is not passed, function will use last known hashtable generated by New-VPASToken .PARAMETER TargetMetric Specify which report will be run Possible values: PSMSessionsInXDays, PSMUtilizationForXDays, PSMConnectionComponentsInXDays, UsersConnectingWithPSMInXDays .PARAMETER MetricFormat Specify the report output format NONE will return the generated hashtable of data that can be assigned to a variable Possible values: JSON, HTML, ALL, NONE .PARAMETER OutputDirectory Specify where the location for report output to be saved .PARAMETER DayRange Specify the date range for the selected metric report .PARAMETER AmtOfSets Specify the length of historic data to be included in the metric report .PARAMETER AmtOfUsers Specify the amount of users to be included in the metric .PARAMETER HideRawData Removes the RawData visual from the exported output Helpful when exporting to a PDF or document to remove extra not needed information .PARAMETER IgnorePlatforms Wildcard value that will cause a record to be ignored from the metrics if the target record PlatformID matches .PARAMETER IgnoreUsernames Wildcard value that will cause a record to be ignored from the metrics if the target record Username matches .PARAMETER PlatformSearchQuery Wildcard value that will limit the metrics to only target records that match the searchquery via platformID .PARAMETER UsernameSearchQuery Wildcard value that will limit the metrics to only target records that match the searchquery via account username .PARAMETER InputParameters HashTable of values containing the parameters required to make the API call .EXAMPLE $GenerateReport = Invoke-VPASMetricsPSM -TargetMetric PSMConnectionComponentsInXDays -MetricFormat ALL -OutputDirectory C:\Temp\Metrics\PSMMetrics -DayRange 30 .EXAMPLE $GenerateReport = Invoke-VPASMetricsPSM -TargetMetric PSMSessionsInXDays -MetricFormat ALL -OutputDirectory C:\Temp\Metrics\PSMMetrics -DayRange 7 -AmtOfSets 4 .EXAMPLE $GenerateReport = Invoke-VPASMetricsPSM -TargetMetric PSMUtilizationForXDays -MetricFormat ALL -OutputDirectory C:\Temp\Metrics\PSMMetrics -DayRange 30 .EXAMPLE $GenerateReport = Invoke-VPASMetricsPSM -TargetMetric UsersConnectingWithPSMInXDays -MetricFormat ALL -OutputDirectory C:\Temp\Metrics\PSMMetrics -DayRange 30 -AmtOfUsers 10 .EXAMPLE $InputParameters = @{ TargetMetric = "PSMSessionsInXDays"|"PSMUtilizationForXDays"|"PSMConnectionComponentsInXDays"|"UsersConnectingWithPSMInXDays" MetricFormat = "JSON"|"HTML"|"ALL"|"NONE" OutputDirectory = "C:\temp\ReportOutputs" DayRange = "7" AmtOfSets = "4" HideRawData = $true|$false AmtOfUsers = "5" IgnorePlatforms = @("WinDomain","WinLocal") IgnoreUsernames = @("IgnoreUsername1") PlatformSearchQuery = @("norotate","store") UsernameSearchQuery = @("TargetUser1","TargetUser3") } $GenerateReport = Invoke-VPASMetricsPSM -InputParameters $InputParameters .OUTPUTS HashTable object if successful --- $false if failed #> function Invoke-VPASMetricsPSM{ [OutputType([bool])] [CmdletBinding(DefaultParameterSetName='Set1')] Param( [Parameter(Mandatory=$true,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true,HelpMessage="Enter TargetMetric to be generated (PSMSessionsInXDays,PSMUtilizationForXDays,PSMConnectionComponentsInXDays,UsersConnectingWithPSMInXDays)")] [ValidateSet('PSMSessionsInXDays','PSMUtilizationForXDays','PSMConnectionComponentsInXDays','UsersConnectingWithPSMInXDays')] [String]$TargetMetric, [Parameter(Mandatory=$true,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true,HelpMessage="Enter ReportOutput type (JSON, HTML, ALL, NONE)")] [ValidateSet('JSON','HTML','ALL','NONE')] [String]$MetricFormat, [Parameter(Mandatory=$false,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true)] [String]$OutputDirectory, [Parameter(Mandatory=$true,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true,HelpMessage="Specify the date range for the selected metric report")] [String]$DayRange, [Parameter(Mandatory=$false,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true)] [String]$AmtOfSets, [Parameter(Mandatory=$false,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true)] [String]$AmtOfUsers, [Parameter(Mandatory=$false,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true)] [switch]$HideRawData, [Parameter(Mandatory=$false,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true)] [String[]]$IgnorePlatforms, [Parameter(Mandatory=$false,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true)] [String[]]$IgnoreUsernames, [Parameter(Mandatory=$false,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true)] [String[]]$PlatformSearchQuery, [Parameter(Mandatory=$false,ParameterSetName='Set1',ValueFromPipelineByPropertyName=$true)] [String[]]$UsernameSearchQuery, [Parameter(Mandatory=$true,ParameterSetName='InputParameters',ValueFromPipelineByPropertyName=$true,HelpMessage="Hashtable of parameters required to make API call, refer to get-help -examples for valid inputs")] [hashtable]$InputParameters, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true)] [hashtable]$token ) Begin{ $tokenval,$sessionval,$PVWA,$Header,$ISPSS,$IdentityURL,$EnableTextRecorder,$AuditTimeStamp,$NoSSL,$VaultVersion,$HideWarnings,$AuthenticatedAs,$SubDomain,$EnableTroubleshooting = Get-VPASSession -token $token $CommandName = $MyInvocation.MyCommand.Name $log = Write-VPASTextRecorder -inputval $CommandName -token $token -LogType COMMAND } Process{ try{ if($PSCmdlet.ParameterSetName -eq "InputParameters"){ $KeyHash = @{ set1 = @{ AcceptableKeys = @("TargetMetric","MetricFormat","OutputDirectory","DayRange","AmtOfSets","AmtOfUsers","HideRawData","IgnorePlatforms","IgnoreUsernames","PlatformSearchQuery","UsernameSearchQuery") MandatoryKeys = @("TargetMetric","MetricFormat") } } $CheckSet = Test-VPASHashtableKeysHelper -InputHash $InputParameters -KeyHash $KeyHash if(!$CheckSet){ $log = Write-VPASTextRecorder -inputval "FAILED TO FIND TARGET PARAMETER SET" -token $token -LogType MISC Write-Verbose "FAILED TO FIND TARGET PARAMETER SET" Write-VPASOutput -str "FAILED TO FIND TARGET PARAMETER SET...VIEW EXAMPLES BELOW:" -type E $examples = Write-VPASExampleHelper -CommandName $CommandName return $false } else{ foreach($key in $InputParameters.Keys){ Set-Variable -Name $key -Value $InputParameters.$key } } } }catch{ $log = Write-VPASTextRecorder -inputval $_ -token $token -LogType ERROR $log = Write-VPASTextRecorder -inputval "REST API COMMAND RETURNED: FALSE" -token $token -LogType MISC Write-Verbose "FAILED TO INVOKE METRIC" Write-VPASOutput -str $_ -type E return $false } try{ $SkipPlatformsSTR = "" foreach($rec in $IgnorePlatforms){ $SkipPlatformsSTR += "*" + $rec + "*; " } $SkipUsernameSTR = "" foreach($rec in $IgnoreUsernames){ $SkipUsernameSTR += "*" + $rec + "*; " } $SkipSafeSTR = "" foreach($rec in $IgnoreSafes){ $SkipSafeSTR += "*" + $rec + "*; " } $TargetSafeSTR = "" foreach($rec in $SafeSearchQuery){ $TargetSafeSTR += "*" + $rec + "*; " } $TargetPlatformSTR = "" foreach($rec in $PlatformSearchQuery){ $TargetPlatformSTR += "*" + $rec + "*; " } $TargetUsernameSTR = "" foreach($rec in $UsernameSearchQuery){ $TargetUsernameSTR += "*" + $rec + "*; " } if($MetricFormat -ne "NONE"){ if([String]::IsNullOrEmpty($OutputDirectory)){ $curUser = $env:UserName $OutputDirectory = "C:\Users\$curUser\AppData\Local\VPASModuleOutputs\Metrics" Write-Verbose "NO OUTPUT DIRECTORY SUPPLIED, USING DEFAULT LOCATION: $OutputDirectory" if(Test-Path -Path $OutputDirectory){ #DO NOTHING } else{ write-verbose "$OutputDirectory DOES NOT EXIST, CREATING DIRECTORY" $MakeDirectory = New-Item -Path $OutputDirectory -Type Directory } } else{ if(Test-Path -Path $OutputDirectory){ #DO NOTHING } else{ $curUser = $env:UserName $OutputDirectory = "C:\Users\$curUser\AppData\Local\VPASModuleOutputs\Metrics" write-verbose "$OutputDirectory DOES NOT EXIST, USING DEFAULT LOCATION: $OutputDirectory" if(Test-Path -Path $OutputDirectory){ #DO NOTHING } else{ $MakeDirectory = New-Item -Path $OutputDirectory -Type Directory } } } } if([String]::IsNullOrEmpty($HTMLChart)){ $HTMLChart = "PieChart" } if($TargetMetric -eq "PSMSessionsInXDays"){ $tagout = "sessions" if([String]::IsNullOrEmpty($DayRange)){ Write-VPASOutput -str "NO DayRange SUPPLIED, ENTER DayRange (1 - 365): " -type Y $DayRange = Read-Host } if([String]::IsNullOrEmpty($AmtOfSets)){ Write-VPASOutput -str "NO AmtOfSets SUPPLIED, ENTER AmtOfSets (1 - 365): " -type Y $AmtOfSets = Read-Host } try{ $AmtDaysInt = [Int]$DayRange if($AmtDaysInt -lt 1 -or $AmtDaysInt -gt 365){ Write-Verbose "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" -type E return $false } }catch{ Write-Verbose "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" -type E Write-VPASOutput -str $_ -type E return $false } try{ $AmtSetsInt = [Int]$AmtOfSets if($AmtSetsInt -lt 1 -or $AmtSetsInt -gt 365){ Write-Verbose "INVALID INPUT FOR SET AMOUNT: $AmtOfSets...MUST AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR SET AMOUNT: $AmtOfSets...MUST AN INTEGER BETWEEN 1 - 365" -type E return $false } }catch{ Write-Verbose "INVALID INPUT FOR SET AMOUNT: $AmtOfSets ...MUST AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR SET AMOUNT: $AmtOfSets ...MUST AN INTEGER BETWEEN 1 - 365" -type E Write-VPASOutput -str $_ -type E return $false } #INITIALIZE DATE HASH $RecordingsHash = @{} $CurTime = ([int][double]::Parse((Get-Date (get-date (get-date).Date).ToLocalTime() -UFormat %s))) + 86400 $NumSeconds = ($AmtDaysInt * 86400) $counter = 0 while($counter -lt $AmtSetsInt){ $counter += 1 $MaxNum = $CurTime - 1 $CurTime = $CurTime - $NumSeconds $MinNum = $CurTime $UniqueKey = "Set" + $counter $RecordingsHash += @{ $UniqueKey = @{ Max = $MaxNum Min = $MinNum Count = 0 RawData = @() } } } $LastKey = $AmtSetsInt $LastKeyStr = "Set$LastKey" $FromTime = $RecordingsHash.$LastKeyStr.Min $ToTime = $RecordingsHash.Set1.Max #GET RECORDINGS $AllRecordings = Get-VPASPSMSessions -SearchQuery " " -FromTime $FromTime -ToTime $ToTime if(!$AllRecordings){ Write-Verbose "FAILED TO RETRIEVE PSM SESSIONS" Write-VPASOutput -str "FAILED TO RETRIEVE PSM SESSIONS" -type E Write-VPASOutput -str $_ -type E return $false } foreach($rec in $AllRecordings.Recordings){ $validtargetplatform = $false $validtargetusername = $false $EpochStart = $rec.Start $targetPlat = $rec.AccountPlatformID $targetUsername = $rec.AccountUsername $targetAddress = $rec.AccountAddress $skipval = $false if($PlatformSearchQuery.Count -eq 0){ $validtargetplatform = $true } else{ foreach($query in $PlatformSearchQuery){ if($targetPlat -match $query){ $validtargetplatform = $true } } } if($UsernameSearchQuery.Count -eq 0){ $validtargetusername = $true } else{ foreach($query in $UsernameSearchQuery){ if($targetUsername -match $query){ $validtargetusername = $true } } } if(!$validtargetplatform -or !$validtargetusername){ $skipval = $true } if(!$skipval){ foreach($ignoreval in $IgnorePlatforms){ if($targetPlat -match $ignoreval){ Write-Verbose "SKIPPING $targetUsername@$targetAddress : SKIP VALUE $ignoreval FOUND IN PLATFORM $targetPlat" $skipval = $true } } } if(!$skipval){ foreach($ignoreval in $IgnoreUsernames){ if($targetUsername -match $ignoreval){ Write-Verbose "SKIPPING $targetUsername@$targetAddress : SKIP VALUE $ignoreval FOUND IN USERNAME $targetUsername" $skipval = $true } } } if(!$skipval){ $parser = $AmtSetsInt while($parser -gt 0){ $uniqueKey = "Set$parser" $setmin = $RecordingsHash.$uniqueKey.Min $setmax = $RecordingsHash.$uniqueKey.Max $setcount = $RecordingsHash.$uniqueKey.Count if($EpochStart -ge $setmin -and $EpochStart -le $setmax){ $setcount += 1 $RecordingsHash.$uniqueKey.Count = $setcount $parser = 0 $RecordingsHash.$UniqueKey.RawData += $rec } else{ $parser -= 1 } } } } #CONVERT THINGS TO HUMAN READABLE TIME $origin = New-Object -Type DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0 $parser = $AmtSetsInt while($parser -gt 0){ $uniqueKey = "Set$parser" $setmin = $RecordingsHash.$uniqueKey.Min $setmax = $RecordingsHash.$uniqueKey.Max $newmin = $origin.AddSeconds($setmin).ToShortDateString() $newmax = $origin.AddSeconds($setmax).ToShortDateString() $RecordingsHash.$uniqueKey.Min = $newmin $RecordingsHash.$uniqueKey.Max = $newmax $parser -= 1 } if($MetricFormat -eq "JSON" -or $MetricFormat -eq "ALL"){ $outputfile = "$OutputDirectory/PSMSessionsIn" + $AmtDaysInt + "Days.json" $OutputDataJSON = $RecordingsHash | ConvertTo-Json -Depth 100 Write-Output $OutputDataJSON | Set-Content $outputfile } if($MetricFormat -eq "NONE"){ $htmlData += @{ datahash = $RecordingsHash } } if($MetricFormat -eq "HTML" -or $MetricFormat -eq "ALL"){ $outputfile = "$OutputDirectory/PSMSessionsIn" + $AmtDaysInt + "Days.html" $titlesplit = "PSM Sessions Made Per $AmtDaysInt Days" $metricTag = "PSM Metrics" $recommendation1 = "PSM provides a secure gateway for accessing critical systems and applications by privileged users. It ensures that sensitive credentials are never exposed to end-users, reducing the risk of credential theft and unauthorized access." $recommendation2 = "PSM facilitates sessions that are isolated within the PSM environment, reducing the risk of lateral movement by attackers and preventing unauthorized access to sensitive resources." $recommendation3 = "PSM offers real-time monitoring and alerting capabilities, allowing organizations to detect and respond to suspicious activities promptly. It provides visibility into privileged sessions, user activities, and system events, enabling proactive threat detection and incident response." $OutputDataJSON = $RecordingsHash | ConvertTo-Json -Depth 100 $curTime = get-date -Format "MM/dd/yyyy HH:mm:ss" if($HTMLChart -eq "ALL"){ $tablestr = "Bar Graph, Line Graph, Pie Chart" } else{ $tablestr = $HTMLChart } $metricexplanation = "This metric tracks the quantity of PSM sessions made within the past $AmtDaysInt days in batches, with historical data included." $tempstr = "" $tempstr2 = "" $tempstr3 = "" $tempstr4 = "" $AllKeys = $RecordingsHash.Keys $AmtKeys = $AllKeys.Count $counter = $AmtKeys $GetMinMax = @() while($counter -gt 0){ $key = "Set$counter" $minVal = $RecordingsHash.$key.Min $maxVal = $RecordingsHash.$key.Max $curCount = $RecordingsHash.$key.count $GetMinMax += $curCount $textColor = '#{0:X6}' -f (Get-Random -Maximum 0x1000000) $outputstr = "$maxVal - $minVal" $tempstr += "`"$outputstr`"," $tempstr2 += "$curCount," $tempstr3 += "`"green`"," $tempstr4 += "`"$textColor`"," $counter -= 1 } if($GetMinMax.count -ne 0){ $GetMinMax = $GetMinMax | Sort-Object $mintick = 0 $maxtick = $GetMinMax[$GetMinMax.Count - 1] } else{ $mintick = 0 $maxtick = 0 } if(![String]::IsNullOrEmpty($tempstr)){ $tempstr = $tempstr.Substring(0,$tempstr.Length-1) } if(![String]::IsNullOrEmpty($tempstr2)){ $tempstr2 = $tempstr2.Substring(0,$tempstr2.Length-1) } if(![String]::IsNullOrEmpty($tempstr3)){ $tempstr3 = $tempstr3.Substring(0,$tempstr3.Length-1) } if(![String]::IsNullOrEmpty($tempstr4)){ $tempstr4 = $tempstr4.Substring(0,$tempstr4.Length-1) } $htmlData += @{ Recommendation1 = $recommendation1 Recommendation2 = $recommendation2 Recommendation3 = $recommendation3 titlesplit = $titlesplit outputfile = $outputfile metricTag = $metricTag OutputDataJSON = $OutputDataJSON curTime = $curTime tablestr = $tablestr metricexplanation = $metricexplanation HTMLChart = $HTMLChart tempstr = $tempstr tempstr2 = $tempstr2 tempstr3 = $tempstr3 tempstr4 = $tempstr4 datahash = $RecordingsHash maxtick = $maxtick mintick = $mintick } } } if($TargetMetric -eq "PSMUtilizationForXDays"){ $tagout = "connections" if([String]::IsNullOrEmpty($DayRange)){ Write-VPASOutput -str "NO DayRange SUPPLIED, ENTER DayRange (1 - 365): " -type Y $DayRange = Read-Host } try{ $AmtDaysInt = [Int]$DayRange if($AmtDaysInt -lt 1 -or $AmtDaysInt -gt 365){ Write-Verbose "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" -type E return $false } }catch{ Write-Verbose "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" -type E Write-VPASOutput -str $_ -type E return $false } #INITIALIZE HASH $RecordingsHash = @{} $MaxTime = ([int][double]::Parse((Get-Date (get-date (get-date).Date).ToLocalTime() -UFormat %s))) + 86400 $TimeDiff = ($AmtDaysInt * 86400) $MinTime = $MaxTime - $TimeDiff $AllPSMs = Get-VPASSystemHealth -Component PSM if(!$AllPSMs){ Write-Verbose "FAILED TO RETRIEVE PSM IDs" Write-VPASOutput -str "FAILED TO RETRIEVE PSM IDs" -type E Write-VPASOutput -str $_ -type E return $false } $FromTime = $MinTime $ToTime = $MaxTime foreach($rec in $AllPSMs.ComponentsDetails){ $ComponentUsername = $rec.ComponentUserName if($ComponentUsername -match "PSMApp_"){ $UniqueKey = $ComponentUsername $RecordingsHash += @{ $UniqueKey = @{ Count = 0 RawData = @() } } } } #GET RECORDINGS $AllRecordings = Get-VPASPSMSessions -SearchQuery " " -FromTime $FromTime -ToTime $ToTime if(!$AllRecordings){ Write-Verbose "FAILED TO RETRIEVE PSM SESSIONS" Write-VPASOutput -str "FAILED TO RETRIEVE PSM SESSIONS" -type E Write-VPASOutput -str $_ -type E return $false } foreach($rec in $AllRecordings.Recordings){ $validtargetplatform = $false $validtargetusername = $false $EpochStart = $rec.Start $TargetPSM = $rec.RawProperties.ProviderID $targetPlat = $rec.AccountPlatformID $targetUsername = $rec.AccountUsername $targetAddress = $rec.AccountAddress $skipval = $false if($PlatformSearchQuery.Count -eq 0){ $validtargetplatform = $true } else{ foreach($query in $PlatformSearchQuery){ if($targetPlat -match $query){ $validtargetplatform = $true } } } if($UsernameSearchQuery.Count -eq 0){ $validtargetusername = $true } else{ foreach($query in $UsernameSearchQuery){ if($targetUsername -match $query){ $validtargetusername = $true } } } if(!$validtargetplatform -or !$validtargetusername){ $skipval = $true } if(!$skipval){ foreach($ignoreval in $IgnorePlatforms){ if($targetPlat -match $ignoreval){ Write-Verbose "SKIPPING $targetUsername@$targetAddress : SKIP VALUE $ignoreval FOUND IN PLATFORM $targetPlat" $skipval = $true } } } if(!$skipval){ foreach($ignoreval in $IgnoreUsernames){ if($targetUsername -match $ignoreval){ Write-Verbose "SKIPPING $targetUsername@$targetAddress : SKIP VALUE $ignoreval FOUND IN USERNAME $targetUsername" $skipval = $true } } } if(!$skipval){ if($EpochStart -le $MaxTime -and $EpochStart -ge $MinTime){ if($RecordingsHash.$TargetPSM){ $curCount = $RecordingsHash.$TargetPSM.Count $curCount += 1 $RecordingsHash.$TargetPSM.Count = $curCount $RecordingsHash.$TargetPSM.RawData += $rec } } } } if($MetricFormat -eq "JSON" -or $MetricFormat -eq "ALL"){ $outputfile = "$OutputDirectory/PSMUtilizationFor" + $AmtDaysInt + "Days.json" $OutputDataJSON = $RecordingsHash | ConvertTo-Json -Depth 100 Write-Output $OutputDataJSON | Set-Content $outputfile } if($MetricFormat -eq "NONE"){ $htmlData += @{ datahash = $RecordingsHash } } if($MetricFormat -eq "HTML" -or $MetricFormat -eq "ALL"){ $outputfile = "$OutputDirectory/PSMUtilizationFor" + $AmtDaysInt + "Days.html" $titlesplit = "PSM Utilization For $AmtDaysInt Days" $metricTag = "PSM Metrics" $recommendation1 = "Load balancing ensures high availability of PSM services by distributing incoming traffic across multiple PSM instances. In the event of a server failure or maintenance, the load balancer automatically redirects traffic to healthy instances, minimizing downtime and ensuring uninterrupted access to privileged resources." $recommendation2 = "Load balancing optimizes resource utilization and performance by evenly distributing incoming requests across multiple PSM instances. This prevents any single instance from becoming overloaded, ensuring consistent response times and minimizing latency for users accessing privileged sessions." $recommendation3 = "Load balancing enables organizations to deploy PSM instances across multiple geographic locations for enhanced redundancy and disaster recovery. By using global load balancing techniques, organizations can route traffic to the nearest or least congested PSM instance, improving performance and reliability for remote users." $OutputDataJSON = $RecordingsHash | ConvertTo-Json -Depth 100 $curTime = get-date -Format "MM/dd/yyyy HH:mm:ss" if($HTMLChart -eq "ALL"){ $tablestr = "Bar Graph, Line Graph, Pie Chart" } else{ $tablestr = $HTMLChart } $metricexplanation = "This metric tracks which PSM is being used in an evironment with multiple/loadbalanced PSMs" #PSMUtilizationForXDays $tempstr = "" $tempstr2 = "" $tempstr3 = "" $tempstr4 = "" $AllKeys = $RecordingsHash.Keys $GetMinMax = @() foreach($key in $AllKeys){ $curCount = $RecordingsHash.$key.count $GetMinMax += $curCount $textColor = '#{0:X6}' -f (Get-Random -Maximum 0x1000000) $tempstr += "`"$key`"," $tempstr2 += "$curCount," $tempstr3 += "`"green`"," $tempstr4 += "`"$textColor`"," } if($GetMinMax.count -ne 0){ $GetMinMax = $GetMinMax | Sort-Object $mintick = 0 $maxtick = $GetMinMax[$GetMinMax.Count - 1] } else{ $mintick = 0 $maxtick = 0 } if(![String]::IsNullOrEmpty($tempstr)){ $tempstr = $tempstr.Substring(0,$tempstr.Length-1) } if(![String]::IsNullOrEmpty($tempstr2)){ $tempstr2 = $tempstr2.Substring(0,$tempstr2.Length-1) } if(![String]::IsNullOrEmpty($tempstr3)){ $tempstr3 = $tempstr3.Substring(0,$tempstr3.Length-1) } if(![String]::IsNullOrEmpty($tempstr4)){ $tempstr4 = $tempstr4.Substring(0,$tempstr4.Length-1) } $htmlData += @{ Recommendation1 = $recommendation1 Recommendation2 = $recommendation2 Recommendation3 = $recommendation3 titlesplit = $titlesplit outputfile = $outputfile metricTag = $metricTag OutputDataJSON = $OutputDataJSON curTime = $curTime tablestr = $tablestr metricexplanation = $metricexplanation HTMLChart = $HTMLChart tempstr = $tempstr tempstr2 = $tempstr2 tempstr3 = $tempstr3 tempstr4 = $tempstr4 datahash = $RecordingsHash maxtick = $maxtick mintick = $mintick } } } if($TargetMetric -eq "PSMConnectionComponentsInXDays"){ $tagout = "connections" if([String]::IsNullOrEmpty($DayRange)){ Write-VPASOutput -str "NO DayRange SUPPLIED, ENTER DayRange (1 - 365): " -type Y $DayRange = Read-Host } try{ $AmtDaysInt = [Int]$DayRange if($AmtDaysInt -lt 1 -or $AmtDaysInt -gt 365){ Write-Verbose "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" -type E return $false } }catch{ Write-Verbose "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" -type E Write-VPASOutput -str $_ -type E return $false } #INITIALIZE HASH $RecordingsHash = @{} $MaxTime = ([int][double]::Parse((Get-Date (get-date (get-date).Date).ToLocalTime() -UFormat %s))) + 86400 $TimeDiff = ($AmtDaysInt * 86400) $MinTime = $MaxTime - $TimeDiff $FromTime = $MinTime $ToTime = $MaxTime #GET RECORDINGS $AllRecordings = Get-VPASPSMSessions -SearchQuery " " -FromTime $FromTime -ToTime $ToTime if(!$AllRecordings){ Write-Verbose "FAILED TO RETRIEVE PSM SESSIONS" Write-VPASOutput -str "FAILED TO RETRIEVE PSM SESSIONS" -type E Write-VPASOutput -str $_ -type E return $false } foreach($rec in $AllRecordings.Recordings){ $validtargetplatform = $false $validtargetusername = $false $EpochStart = $rec.Start $TargetCC = $rec.ConnectionComponentID $targetPlat = $rec.AccountPlatformID $targetUsername = $rec.AccountUsername $targetAddress = $rec.AccountAddress $skipval = $false if($PlatformSearchQuery.Count -eq 0){ $validtargetplatform = $true } else{ foreach($query in $PlatformSearchQuery){ if($targetPlat -match $query){ $validtargetplatform = $true } } } if($UsernameSearchQuery.Count -eq 0){ $validtargetusername = $true } else{ foreach($query in $UsernameSearchQuery){ if($targetUsername -match $query){ $validtargetusername = $true } } } if(!$validtargetplatform -or !$validtargetusername){ $skipval = $true } if(!$skipval){ foreach($ignoreval in $IgnorePlatforms){ if($targetPlat -match $ignoreval){ Write-Verbose "SKIPPING $targetUsername@$targetAddress : SKIP VALUE $ignoreval FOUND IN PLATFORM $targetPlat" $skipval = $true } } } if(!$skipval){ foreach($ignoreval in $IgnoreUsernames){ if($targetUsername -match $ignoreval){ Write-Verbose "SKIPPING $targetUsername@$targetAddress : SKIP VALUE $ignoreval FOUND IN USERNAME $targetUsername" $skipval = $true } } } if(!$skipval){ if($EpochStart -le $MaxTime -and $EpochStart -ge $MinTime){ if($RecordingsHash.$TargetCC){ $curCount = $RecordingsHash.$TargetCC.Count $curCount += 1 $RecordingsHash.$TargetCC.Count = $curCount $RecordingsHash.$TargetCC.RawData += $rec } else{ $RecordingsHash += @{ $TargetCC = @{ Count = 1 RawData = @($rec) } } } } } } if($MetricFormat -eq "JSON" -or $MetricFormat -eq "ALL"){ $outputfile = "$OutputDirectory/PSMConnectionComponentsFor" + $AmtDaysInt + "Days.json" $OutputDataJSON = $RecordingsHash | ConvertTo-Json -Depth 100 Write-Output $OutputDataJSON | Set-Content $outputfile } if($MetricFormat -eq "NONE"){ $htmlData += @{ datahash = $RecordingsHash } } if($MetricFormat -eq "HTML" -or $MetricFormat -eq "ALL"){ $outputfile = "$OutputDirectory/PSMConnectionComponentsFor" + $AmtDaysInt + "Days.html" $titlesplit = "PSM Connection Components Used In The Last $AmtDaysInt Days" $metricTag = "PSM Metrics" $recommendation1 = "Creating connection components for PSM allows sessions to be recorded, including keystrokes, commands, and screen activities, providing a detailed audit trail of user actions. This audit trail is invaluable for compliance purposes, forensic analysis, and investigating security incidents." $recommendation2 = "When utilizing a connection component to initiate a privileged session through PSM, the platform dynamically injects the credentials into the session without revealing them to the end-user. This process ensures that sensitive passwords are never exposed to users or applications accessing the target system." $recommendation3 = "Connection components reduce the attack surface by limiting direct access to target systems and enforcing strict access controls. This minimizes the risk of unauthorized access, privilege escalation, and insider threats, improving overall security posture." $OutputDataJSON = $RecordingsHash | ConvertTo-Json -Depth 100 $curTime = get-date -Format "MM/dd/yyyy HH:mm:ss" if($HTMLChart -eq "ALL"){ $tablestr = "Bar Graph, Line Graph, Pie Chart" } else{ $tablestr = $HTMLChart } $metricexplanation = "This metric tracks which connection component was used to make a PSM session.<br>(Note - this will only track successful PSM sessions)" $tempstr = "" $tempstr2 = "" $tempstr3 = "" $tempstr4 = "" $AllKeys = $RecordingsHash.Keys $GetMinMax = @() foreach($key in $AllKeys){ $curCount = $RecordingsHash.$key.count $GetMinMax += $curCount $textColor = '#{0:X6}' -f (Get-Random -Maximum 0x1000000) $tempstr += "`"$key`"," $tempstr2 += "$curCount," $tempstr3 += "`"green`"," $tempstr4 += "`"$textColor`"," } if($GetMinMax.count -ne 0){ $GetMinMax = $GetMinMax | Sort-Object $mintick = 0 $maxtick = $GetMinMax[$GetMinMax.Count - 1] } else{ $mintick = 0 $maxtick = 0 } if(![String]::IsNullOrEmpty($tempstr)){ $tempstr = $tempstr.Substring(0,$tempstr.Length-1) } if(![String]::IsNullOrEmpty($tempstr2)){ $tempstr2 = $tempstr2.Substring(0,$tempstr2.Length-1) } if(![String]::IsNullOrEmpty($tempstr3)){ $tempstr3 = $tempstr3.Substring(0,$tempstr3.Length-1) } if(![String]::IsNullOrEmpty($tempstr4)){ $tempstr4 = $tempstr4.Substring(0,$tempstr4.Length-1) } $htmlData += @{ Recommendation1 = $recommendation1 Recommendation2 = $recommendation2 Recommendation3 = $recommendation3 titlesplit = $titlesplit outputfile = $outputfile metricTag = $metricTag OutputDataJSON = $OutputDataJSON curTime = $curTime tablestr = $tablestr metricexplanation = $metricexplanation HTMLChart = $HTMLChart tempstr = $tempstr tempstr2 = $tempstr2 tempstr3 = $tempstr3 tempstr4 = $tempstr4 datahash = $RecordingsHash maxtick = $maxtick mintick = $mintick } } } if($TargetMetric -eq "UsersConnectingWithPSMInXDays"){ $tagout = "connections" if([String]::IsNullOrEmpty($DayRange)){ Write-VPASOutput -str "NO DayRange SUPPLIED, ENTER DayRange (1 - 365): " -type Y $DayRange = Read-Host } try{ $AmtDaysInt = [Int]$DayRange if($AmtDaysInt -lt 1 -or $AmtDaysInt -gt 365){ Write-Verbose "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" -type E return $false } }catch{ Write-Verbose "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" Write-VPASOutput -str "INVALID INPUT FOR DAY RANGE: $DayRange ...MUST BE AN INTEGER BETWEEN 1 - 365" -type E Write-VPASOutput -str $_ -type E return $false } if([String]::IsNullOrEmpty($AmtOfUsers)){ Write-VPASOutput -str "NO AmtOfUsers SUPPLIED, ENTER AmtOfUsers TO DISPLAY [ALL]: " -type Y $AmtOfUsers = Read-Host if([String]::IsNullOrEmpty($AmtOfUsers)){$AmtOfUsers = "ALL"} } try{ if($AmtOfUsers -eq "ALL"){ $AmtOfUsersInt = 9999 } else{ $AmtOfUsersInt = [Int]$AmtOfUsers if($AmtOfUsersInt -lt 1){ Write-Verbose "INVALID INPUT FOR AMOUNT OF USERS: $AmtOfUsers ...MUST BE A VALID INTEGER ABOVE 0 OR 'ALL'" Write-VPASOutput -str "INVALID INPUT FOR AMOUNT OF USERS: $AmtOfUsers ...MUST BE A VALID INTEGER ABOVE 0 OR 'ALL'" -type E return $false } } }catch{ Write-Verbose "INVALID INPUT FOR AMOUNT OF USERS: $AmtOfUsers ...MUST BE A VALID INTEGER ABOVE 0 OR 'ALL'" Write-VPASOutput -str "INVALID INPUT FOR AMOUNT OF USERS: $AmtOfUsers ...MUST BE A VALID INTEGER ABOVE 0 OR 'ALL'" -type E Write-VPASOutput -str $_ -type E return $false } #INITIALIZE HASH $RecordingsHashTemp = @{} $MaxTime = ([int][double]::Parse((Get-Date (get-date (get-date).Date).ToLocalTime() -UFormat %s))) + 86400 $TimeDiff = ($AmtDaysInt * 86400) $MinTime = $MaxTime - $TimeDiff $FromTime = $MinTime $ToTime = $MaxTime #GET RECORDINGS $AllRecordings = Get-VPASPSMSessions -SearchQuery " " -FromTime $FromTime -ToTime $ToTime if(!$AllRecordings){ Write-Verbose "FAILED TO RETRIEVE PSM SESSIONS" Write-VPASOutput -str "FAILED TO RETRIEVE PSM SESSIONS" -type E Write-VPASOutput -str $_ -type E return $false } foreach($rec in $AllRecordings.Recordings){ $validtargetplatform = $false $validtargetusername = $false $EpochStart = $rec.Start $TargetUser = $rec.User $targetPlat = $rec.AccountPlatformID $targetUsername = $rec.AccountUsername $targetAddress = $rec.AccountAddress $skipval = $false if($PlatformSearchQuery.Count -eq 0){ $validtargetplatform = $true } else{ foreach($query in $PlatformSearchQuery){ if($targetPlat -match $query){ $validtargetplatform = $true } } } if($UsernameSearchQuery.Count -eq 0){ $validtargetusername = $true } else{ foreach($query in $UsernameSearchQuery){ if($targetUsername -match $query){ $validtargetusername = $true } } } if(!$validtargetplatform -or !$validtargetusername){ $skipval = $true } if(!$skipval){ foreach($ignoreval in $IgnorePlatforms){ if($targetPlat -match $ignoreval){ Write-Verbose "SKIPPING $targetUsername@$targetAddress : SKIP VALUE $ignoreval FOUND IN PLATFORM $targetPlat" $skipval = $true } } } if(!$skipval){ foreach($ignoreval in $IgnoreUsernames){ if($targetUsername -match $ignoreval){ Write-Verbose "SKIPPING $targetUsername@$targetAddress : SKIP VALUE $ignoreval FOUND IN USERNAME $targetUsername" $skipval = $true } } } if(!$skipval){ if($EpochStart -le $MaxTime -and $EpochStart -ge $MinTime){ if($RecordingsHashTemp.$TargetUser){ $curCount = $RecordingsHashTemp.$TargetUser.Count $curCount += 1 $RecordingsHashTemp.$TargetUser.Count = $curCount $RecordingsHashTemp.$TargetUser.RawData += $rec } else{ $RecordingsHashTemp += @{ $TargetUser = @{ Count = 1 RawData = @($rec) } } } } } } #TRIM TOP USERS $RecordingsHash = @{} $HashUsers = $RecordingsHashTemp.Keys.Count if($HashUsers -le $AmtOfUsersInt){ $RecordingsHash = $RecordingsHashTemp } else{ #TOO MANY USERS RETURNED $sortedUsers = $RecordingsHashTemp.GetEnumerator() | Sort-Object -Property { $_.Value.Count } -Descending $TopKeys = $sortedUsers[0..($AmtOfUsersInt-1)].Key foreach($key in $TopKeys){ $RecordingsHash += @{ $key = $RecordingsHashTemp.$key } } } if($MetricFormat -eq "JSON" -or $MetricFormat -eq "ALL"){ $outputfile = "$OutputDirectory/UsersConnectingWithPSMIn" + $AmtDaysInt + "Days.json" $OutputDataJSON = $RecordingsHash | ConvertTo-Json -Depth 100 Write-Output $OutputDataJSON | Set-Content $outputfile } if($MetricFormat -eq "NONE"){ $htmlData += @{ datahash = $RecordingsHash } } if($MetricFormat -eq "HTML" -or $MetricFormat -eq "ALL"){ $outputfile = "$OutputDirectory/UsersConnectingWithPSMIn" + $AmtDaysInt + "Days.html" $titlesplit = "Users Connecting Via PSM In The Last $AmtDaysInt Days (Top $AmtOfUsers Users)" $metricTag = "PSM Metrics" $recommendation1 = "End users should use the PSM whenever possible to access privileged resources securely without having to manage or remember sensitive credentials. PSM handles the authentication process transparently, simplifying the login experience." $recommendation2 = "End Users should use the PSM to provide an audit trail of user activities. This helps organizations demonstrate compliance with regulatory requirements and internal security policies." $recommendation3 = "End users can have peace of mind knowing that their privileged access is protected by CyberArk's security solution. PSM helps mitigate security risks and ensures that sensitive information remains secure during privileged sessions." $OutputDataJSON = $RecordingsHash | ConvertTo-Json -Depth 100 $curTime = get-date -Format "MM/dd/yyyy HH:mm:ss" if($HTMLChart -eq "ALL"){ $tablestr = "Bar Graph, Line Graph, Pie Chart" } else{ $tablestr = $HTMLChart } $metricexplanation = "This metric tracks which end users are making connections via the PSM<br>(Note - this will only track successful PSM sessions)" $tempstr = "" $tempstr2 = "" $tempstr3 = "" $tempstr4 = "" $AllKeys = $RecordingsHash.Keys $GetMinMax = @() foreach($key in $AllKeys){ $curCount = $RecordingsHash.$key.count $GetMinMax += $curCount $textColor = '#{0:X6}' -f (Get-Random -Maximum 0x1000000) $tempstr += "`"$key`"," $tempstr2 += "$curCount," $tempstr3 += "`"green`"," $tempstr4 += "`"$textColor`"," } if($GetMinMax.count -ne 0){ $GetMinMax = $GetMinMax | Sort-Object $mintick = 0 $maxtick = $GetMinMax[$GetMinMax.Count - 1] } else{ $mintick = 0 $maxtick = 0 } if(![String]::IsNullOrEmpty($tempstr)){ $tempstr = $tempstr.Substring(0,$tempstr.Length-1) } if(![String]::IsNullOrEmpty($tempstr2)){ $tempstr2 = $tempstr2.Substring(0,$tempstr2.Length-1) } if(![String]::IsNullOrEmpty($tempstr3)){ $tempstr3 = $tempstr3.Substring(0,$tempstr3.Length-1) } if(![String]::IsNullOrEmpty($tempstr4)){ $tempstr4 = $tempstr4.Substring(0,$tempstr4.Length-1) } $htmlData += @{ Recommendation1 = $recommendation1 Recommendation2 = $recommendation2 Recommendation3 = $recommendation3 titlesplit = $titlesplit outputfile = $outputfile metricTag = $metricTag OutputDataJSON = $OutputDataJSON curTime = $curTime tablestr = $tablestr metricexplanation = $metricexplanation HTMLChart = $HTMLChart tempstr = $tempstr tempstr2 = $tempstr2 tempstr3 = $tempstr3 tempstr4 = $tempstr4 datahash = $RecordingsHash maxtick = $maxtick mintick = $mintick } } } #ADDING FIX TO LIMIT OUTPUT LABEL QUANTITY...top10 $datalimit = 9 $newtempstrsplit = $tempstr -split "," $newtempstr2split = $tempstr2 -split "," $newtempstr4split = $tempstr4 -split "," if($newtempstrsplit.Count -gt $datalimit){ $stiched = for($i = 0; $i -lt $newtempstrsplit.Count; $i++){ [PSCustomObject]@{ Label = $newtempstrsplit[$i] Value = [int]$newtempstr2split[$i] Color = $newtempstr4split[$i] } } $topValues = $stiched | Sort-Object -Property Value -Descending | Select-Object -First ($newtempstrsplit.Count) $tempstr = "" $tempstr2 = "" $tempstr4 = "" $tempcount = 0 for($i = 0; $i -lt $topValues.Count; $i++){ $splitval = $topValues[$i].Value $splitlabel = $topValues[$i].Label $splitcolor = $topValues[$i].Color if($i -lt $datalimit){ $tempstr += "$splitlabel," $tempstr2 += "$splitval," $tempstr4 += "$splitcolor," } else{ $tempcount += $splitval } } $lastcolor = $topValues[$datalimit].Color $tempstr += "`"Other`"," $tempstr2 += "$tempcount," $tempstr4 += "$lastcolor," if(![String]::IsNullOrEmpty($tempstr)){ $tempstr = $tempstr.Substring(0,$tempstr.Length-1) } if(![String]::IsNullOrEmpty($tempstr2)){ $tempstr2 = $tempstr2.Substring(0,$tempstr2.Length-1) } if(![String]::IsNullOrEmpty($tempstr4)){ $tempstr4 = $tempstr4.Substring(0,$tempstr4.Length-1) } } $datahash = $htmlData.datahash if($MetricFormat -eq "HTML" -or $MetricFormat -eq "ALL"){ #OUTPUT DATA IN A PRETTY HTML write-output " <!DOCTYPE html> <html> <head> <title>$TargetMetric</title> <style> body { font-family: Arial, sans-serif; background-color: #c0c0c0; margin: 0; padding: 20px; } .metrics-container3 { background-color: #333; border-radius: 16px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); padding: 20px; margin: 0; color: white; font-size: 24px; font-weight: bold; Text-align: center; } .metrics-container2 { background-color: #333; border-radius: 16px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); padding: 20px; margin: 0; color: white; } .metrics-container { background-color: #fff; border-radius: 16px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); padding: 20px; margin: 0; } .metric { margin-bottom: 10px; } .metric-label { font-weight: bold; } button { padding: 8px 12px; background-color: #444; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; } button:hover { background-color: #666; } canvas { max-width: 100%; height: auto; } #totalsTable tbody tr:nth-child(even) { background-color: #f2f2f2; } #totalsTable tbody tr:nth-child(odd) { background-color: #ffffff; } .dt-row-dark { background-color: #2c2c2c !important; color: #e0e0e0 !important; } .dt-row-light { background-color: #1a1a1a !important; color: #e0e0e0 !important; } .dataTables_filter input { background-color: #ffffff; color: #000000; border: 1px solid #ccc; border-radius: 6px; padding: 5px 8px; } body.dark-mode .dataTables_filter input { background-color: #2c2c2c; color: #e0e0e0; border: 1px solid #555; } .dataTables_filter input::placeholder { color: #999; } body.dark-mode .dataTables_filter input::placeholder { color: #bbb; } body.dark-mode .dataTables_filter label { color: white; } </style> </head> <link rel=`"stylesheet`" type=`"text/css`" href=`"https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css`"> <script src=`"https://code.jquery.com/jquery-3.7.0.min.js`"></script> <script src=`"https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js`"></script> <script src=`"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js`"></script> <script src=`"https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@0.7.0`"></script> <body> <div style=`"max-width: 1200px; width: 100%; margin: 0 auto;`"> <div style=`"width: 95%; margin-right: 1%; margin-left: 1%; display: flex; justify-content: space-between; align-items: center;`" class=`"metrics-container3`"> <div style=`"margin-left: 20%;`">$titlesplit <small><small>(Powered By Vpas)</small></small></div> <button onclick=`"toggleDarkMode()`" id=`"darkModeBtn`" style=`"padding: 8px 12px; background-color: #444; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold;`">Dark Mode</button> </div> <br> <div style=`"display: flex; width: 99%;`"> <div style=`"width:48%; margin-left: 1%; margin-right: 1%;`" class=`"metrics-container`"> <div class=`"metric`"> <span class=`"metric-label`">Generated By:</span> $AuthenticatedAs </div> <div class=`"metric`"> <span class=`"metric-label`">Generated Date:</span> $curTime </div> <div class=`"metric`"> <span class=`"metric-label`">Metric Category:</span> $metricTag </div> " | Set-content $outputfile if(![String]::IsNullOrEmpty($TargetSafeSTR)){ write-output " <div class=`"metric`"> <span class=`"metric-label`">Target Safe(s):</span> $TargetSafeSTR </div> " | Add-content $outputfile } if(![String]::IsNullOrEmpty($TargetPlatformSTR)){ write-output " <div class=`"metric`"> <span class=`"metric-label`">Target Platform(s):</span> $TargetPlatformSTR </div> " | Add-content $outputfile } if(![String]::IsNullOrEmpty($TargetUsernameSTR)){ write-output " <div class=`"metric`"> <span class=`"metric-label`">Target Username(s):</span> $TargetUsernameSTR </div> " | Add-content $outputfile } if(![String]::IsNullOrEmpty($SkipPlatformsSTR)){ write-output " <div class=`"metric`"> <span class=`"metric-label`">Ignore Platform(s):</span> $SkipPlatformsSTR </div> " | Add-content $outputfile } if(![String]::IsNullOrEmpty($SkipUsernameSTR)){ write-output " <div class=`"metric`"> <span class=`"metric-label`">Ignore Username(s):</span> $SkipUsernameSTR </div> " | Add-content $outputfile } if(![String]::IsNullOrEmpty($SkipSafeSTR)){ write-output " <div class=`"metric`"> <span class=`"metric-label`">Ignore Safe(s):</span> $SkipSafeSTR </div> " | Add-content $outputfile } write-output " </div> <div style=`"width:48%; margin-left: 1%; margin-right: 1;%`" class=`"metrics-container`"> <div class=`"metric`"> <span class=`"metric-label`">Metric Type:</span><small> $titlesplit</small> </div> <div class=`"metric`"> <span class=`"metric-label`">Explanation:</span> <small>$metricexplanation</small> </div> </div> </div> <br> <div style=`"display: flex; align-items: flex-start; justify-content: space-between; max-width: 95%; margin: 0 auto;`" class=`"metrics-container`"> <div style=`"width: 10%;`"> <button onclick=`"toggleLabels()`" style=`"width: 100%; height: 40px;`">Toggle Labels</button> <button onclick=`"regenerateColors()`" style=`"width: 100%; height: 40px; margin-top: 10px;`">Randomize</button> </div> <!-- Chart container --> <div style=`"flex: 1;`"> <canvas id=`"myChartPIE`" width=`"400`" height=`"400`"></canvas> </div> <div style=`"display: flex; flex-direction: column; gap: 10px; margin-left: 20px;`"> <button onclick=`"setChartType('pie')`">Pie</button> <button onclick=`"setChartType('doughnut')`">Doughnut</button> <button onclick=`"setChartType('line')`">Line</button> <button onclick=`"setChartType('bar')`">Bar</button> </div> </div> <br><br> " | Add-content $outputfile Write-Output " <div style=`"max-width: 95%; margin-left: 0.75%; margin-top: -1%; margin-right: 0.75%`" class=`"metrics-container`"> <span class=`"metric-label`">Totals:</span> <div id=`"totalsTableWrapper`" style=`"margin-left: 10%; width: 80%; max-height: 300px; overflow-y: auto; transition: max-height 0.5s ease; color:black;`"> <table id=`"totalsTable`" style=`"width: 100%;`"> <thead> <tr> <th>Value</th> <th>Count</th> </tr> </thead> <tbody> " | Add-Content $outputfile if($TargetMetric -eq "PSMSessionsInXDays"){ $countkeys = $datahash.Keys.Count $i = 1 while($i -le $countkeys){ $Max = $datahash."Set$i".Max $Min = $datahash."Set$i".Min $curCount = $datahash."Set$i".Count $curCount = $datahash."Set$i".Count Write-Output " <tr><td>Set$i) $Max-$Min</td><td>$curCount</td></tr>" | Add-Content $outputfile $i += 1 } } else{ foreach($key in $datahash.Keys){ $curCount = $datahash."$key".count Write-Output " <tr><td>$key</td><td>$curCount</td></tr>" | Add-Content $outputfile } } Write-Output " </tbody> </table> </div> <div style=`"text-align: right; margin-top: 10px;`"> <button id=`"toggleButton`" onclick=`"toggleTable()`">Expand</button> </div> </div> <div style=`"max-width: 95%; margin-left: 0.75%; margin-top: 2%; margin-right: 0.75%`" class=`"metrics-container`"> <div class=`"metric`"> <span class=`"metric-label`">Recommendations:</span> </div> <div class=`"metric`"> <span class=`"metric-label`"><small> 1)</small></span> <small>$recommendation1</small> </div> <div class=`"metric`"> <span class=`"metric-label`"><small> 2)</small></span> <small>$recommendation2</small> </div> <div class=`"metric`"> <span class=`"metric-label`"><small> 3)</small></span> <small>$recommendation3</small> </div> </div> <br> " | Add-Content $outputfile if(!$HideRawData){ Write-Output " <div style=`"max-width:95%; width: 95%; margin-right: 1%; margin-left: 1%`" class=`"metrics-container`"> <div class=`"metric`"> <span class=`"metric-label`">Raw Data:</span> <div><button onclick=`"copyText()`">Copy JSON</button></div> <br> <div style=`"max-width:95%; width: 95%; margin-right: 1%; margin-left: 1%`" class=`"metrics-container2`"> <span id=`"CopyText`" ><small>$OutputDataJSON</small></span> </div> </div> </div> " | Add-Content $outputfile } Write-Output " </div> <script> const xValues = [$tempstr]; const yValues = [$tempstr2]; const colors = [$tempstr4]; let currentChart; let labelsVisible = true; let chartInstance; let currentType = `"pie`"; let darkMode = false; function renderChart(type) { if (chartInstance) { chartInstance.destroy(); } const chartFontColor = darkMode ? '#ffffff' : '#333333'; chartInstance = new Chart(`"myChartPIE`", { type: type, data: { labels: xValues, datasets: [{ backgroundColor: type === 'line' || type === 'bar' ? colors[0] : colors, borderColor: type === 'line' ? (darkMode ? '#ffffff' : '#000000') : '#ffffff', borderWidth: 2, hoverBorderColor: `"#000`", hoverBorderWidth: 2, data: yValues, fill: false }] }, options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10, bottom: 10 } }, title: { display: true, text: `"$titlesplit`", fontSize: 18, fontStyle: `"bold`", fontColor: chartFontColor, padding: 20 }, legend: { display: !(type === 'bar' || type === 'line'), position: 'bottom', labels: { fontColor: chartFontColor, fontSize: 14, padding: 20 } }, plugins: { datalabels: { display: labelsVisible, color: 'white', backgroundColor: 'rgba(0, 0, 0, 0.7)', borderRadius: 6, padding: { top: 6, bottom: 6, left: 10, right: 10 }, font: { weight: 'bold', size: 13 }, formatter: function(value, context) { return context.chart.data.labels[context.dataIndex] + ': ' + value; } } }, tooltips: { backgroundColor: '#000', titleFontSize: 14, bodyFontSize: 13, cornerRadius: 4, caretSize: 6, xPadding: 12, yPadding: 8 }, scales: (type === 'line' || type === 'bar') ? { yAxes: [{ ticks: { beginAtZero: true, fontColor: chartFontColor }, gridLines: { color: '#ccc' } }], xAxes: [{ ticks: { fontColor: chartFontColor }, gridLines: { color: '#ccc' } }] } : {} }, plugins: [ChartDataLabels] }); } function setChartType(type) { currentType = type; renderChart(currentType); } function toggleLabels() { labelsVisible = !labelsVisible; renderChart(currentType); } // Initialize with pie chart Chart.defaults.global.maintainAspectRatio = false; renderChart(currentType); function copyText() { var copyText = document.getElementById(`"CopyText`"); var textArea = document.createElement(`"textarea`"); textArea.value = copyText.textContent; document.body.appendChild(textArea); textArea.select(); document.execCommand(`"Copy`"); textArea.remove(); alert(`"JSON copied to clipboard`"); } function getRandomColor() { // Generate a random color in RGB format const r = Math.floor(Math.random() * 256); const g = Math.floor(Math.random() * 256); const b = Math.floor(Math.random() * 256); return { r, g, b, hex: rgbToHex(r, g, b) }; } function rgbToHex(r, g, b) { return `"#`" + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join(''); } function colorDistance(c1, c2) { // Euclidean distance in RGB space return Math.sqrt( Math.pow(c1.r - c2.r, 2) + Math.pow(c1.g - c2.g, 2) + Math.pow(c1.b - c2.b, 2) ); } function regenerateColors() { const minDistance = 100; // Adjust for stricter color difference const newColors = []; while (newColors.length < xValues.length) { let newColor = getRandomColor(); let isDistinct = newColors.every(existing => colorDistance(existing, newColor) >= minDistance); if (isDistinct) { newColors.push(newColor); } } for (let i = 0; i < newColors.length; i++) { colors[i] = newColors[i].hex; } renderChart(currentType); } `$(document).ready(function() { `$('#totalsTable').DataTable({ paging: false, searching: true, info: false, order: [[1, 'desc']], columnDefs: [ { targets: 1, className: 'dt-body-right' } ] }); }); let expanded = false; function toggleTable() { const wrapper = document.getElementById('totalsTableWrapper'); const button = document.getElementById('toggleButton'); if (!expanded) { wrapper.style.maxHeight = `"none`"; wrapper.style.overflowY = `"visible`"; button.textContent = `"Collapse`"; } else { wrapper.style.maxHeight = `"300px`"; wrapper.style.overflowY = `"auto`"; button.textContent = `"Expand`"; } expanded = !expanded; } function toggleDarkMode() { darkMode = !darkMode; document.body.classList.toggle(`"dark-mode`", darkMode); if (darkMode) { document.body.style.backgroundColor = `"#121212`"; document.body.style.color = `"#e0e0e0`"; document.querySelectorAll('.metrics-container, .metrics-container2, .metrics-container3, #totalsTable thead th').forEach(el => { el.style.backgroundColor = `"#1e1e1e`"; el.style.color = `"#e0e0e0`"; }); document.getElementById('totalsTableWrapper').style.backgroundColor = `"#1e1e1e`"; `$('#totalsTable').DataTable().destroy(); `$('#totalsTable').DataTable({ paging: false, searching: true, info: false, order: [[1, 'desc']], columnDefs: [ { targets: 1, className: 'dt-body-right' } ], `"stripeClasses`": ['dt-row-dark', 'dt-row-light'] }); Chart.defaults.global.defaultFontColor = '#e0e0e0'; renderChart(currentType); document.getElementById('darkModeBtn').innerText = `"Light Mode`"; } else { document.body.style.backgroundColor = `"#c0c0c0`"; document.body.style.color = `"#000`"; document.querySelectorAll('.metrics-container, .metrics-container2, .metrics-container3, #totalsTable thead th').forEach(el => { el.style.backgroundColor = `"#fff`"; el.style.color = `"#000`"; }); document.getElementById('totalsTableWrapper').style.backgroundColor = `"#fff`"; `$('#totalsTable').DataTable().destroy(); `$('#totalsTable').DataTable({ paging: false, searching: true, info: false, order: [[1, 'desc']], columnDefs: [ { targets: 1, className: 'dt-body-right' } ], `"stripeClasses`": [] }); Chart.defaults.global.defaultFontColor = '#333'; renderChart(currentType); document.getElementById('darkModeBtn').innerText = `"Dark Mode`"; } } " | Add-Content $outputfile write-output "</script>`n</body>`n</html>" | Add-Content $outputfile } return $datahash }catch{ Write-Verbose "UNABLE TO RUN REPORT...RETURNING FALSE" Write-VPASOutput -str "UNABLE TO RUN REPORT...RETURNING FALSE" -type E Write-VPASOutput -str $_ -type E return $false } } End{ $log = Write-VPASTextRecorder -inputval $CommandName -token $token -LogType DIVIDER } } # SIG # Begin signature block # MIIrpgYJKoZIhvcNAQcCoIIrlzCCK5MCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUqY/wTWbMRlTm+i3xo3QlE3+Y # FtOggiTgMIIFbzCCBFegAwIBAgIQSPyTtGBVlI02p8mKidaUFjANBgkqhkiG9w0B # AQwFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVy # MRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEh # MB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTIxMDUyNTAwMDAw # MFoXDTI4MTIzMTIzNTk1OVowVjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp # Z28gTGltaXRlZDEtMCsGA1UEAxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5n # IFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjeeUEiIE # JHQu/xYjApKKtq42haxH1CORKz7cfeIxoFFvrISR41KKteKW3tCHYySJiv/vEpM7 # fbu2ir29BX8nm2tl06UMabG8STma8W1uquSggyfamg0rUOlLW7O4ZDakfko9qXGr # YbNzszwLDO/bM1flvjQ345cbXf0fEj2CA3bm+z9m0pQxafptszSswXp43JJQ8mTH # qi0Eq8Nq6uAvp6fcbtfo/9ohq0C/ue4NnsbZnpnvxt4fqQx2sycgoda6/YDnAdLv # 64IplXCN/7sVz/7RDzaiLk8ykHRGa0c1E3cFM09jLrgt4b9lpwRrGNhx+swI8m2J # mRCxrds+LOSqGLDGBwF1Z95t6WNjHjZ/aYm+qkU+blpfj6Fby50whjDoA7NAxg0P # OM1nqFOI+rgwZfpvx+cdsYN0aT6sxGg7seZnM5q2COCABUhA7vaCZEao9XOwBpXy # bGWfv1VbHJxXGsd4RnxwqpQbghesh+m2yQ6BHEDWFhcp/FycGCvqRfXvvdVnTyhe # Be6QTHrnxvTQ/PrNPjJGEyA2igTqt6oHRpwNkzoJZplYXCmjuQymMDg80EY2NXyc # uu7D1fkKdvp+BRtAypI16dV60bV/AK6pkKrFfwGcELEW/MxuGNxvYv6mUKe4e7id # FT/+IAx1yCJaE5UZkADpGtXChvHjjuxf9OUCAwEAAaOCARIwggEOMB8GA1UdIwQY # MBaAFKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQy65Ka/zWWSC8oQEJw # IDaRXBeF5jAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUE # DDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEMGA1Ud # HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj # YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUAA4IBAQASv6Hvi3Sa # mES4aUa1qyQKDKSKZ7g6gb9Fin1SB6iNH04hhTmja14tIIa/ELiueTtTzbT72ES+ # BtlcY2fUQBaHRIZyKtYyFfUSg8L54V0RQGf2QidyxSPiAjgaTCDi2wH3zUZPJqJ8 # ZsBRNraJAlTH/Fj7bADu/pimLpWhDFMpH2/YGaZPnvesCepdgsaLr4CnvYFIUoQx # 2jLsFeSmTD1sOXPUC4U5IOCFGmjhp0g4qdE2JXfBjRkWxYhMZn0vY86Y6GnfrDyo # XZ3JHFuu2PMvdM+4fvbXg50RlmKarkUT2n/cR/vfw1Kf5gZV6Z2M8jpiUbzsJA8p # 1FiAhORFe1rYMIIGFDCCA/ygAwIBAgIQeiOu2lNplg+RyD5c9MfjPzANBgkqhkiG # 9w0BAQwFADBXMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVk # MS4wLAYDVQQDEyVTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIFJvb3QgUjQ2 # MB4XDTIxMDMyMjAwMDAwMFoXDTM2MDMyMTIzNTk1OVowVTELMAkGA1UEBhMCR0Ix # GDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDEsMCoGA1UEAxMjU2VjdGlnbyBQdWJs # aWMgVGltZSBTdGFtcGluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAw # ggGKAoIBgQDNmNhDQatugivs9jN+JjTkiYzT7yISgFQ+7yavjA6Bg+OiIjPm/N/t # 3nC7wYUrUlY3mFyI32t2o6Ft3EtxJXCc5MmZQZ8AxCbh5c6WzeJDB9qkQVa46xiY # Epc81KnBkAWgsaXnLURoYZzksHIzzCNxtIXnb9njZholGw9djnjkTdAA83abEOHQ # 4ujOGIaBhPXG2NdV8TNgFWZ9BojlAvflxNMCOwkCnzlH4oCw5+4v1nssWeN1y4+R # laOywwRMUi54fr2vFsU5QPrgb6tSjvEUh1EC4M29YGy/SIYM8ZpHadmVjbi3Pl8h # JiTWw9jiCKv31pcAaeijS9fc6R7DgyyLIGflmdQMwrNRxCulVq8ZpysiSYNi79tw # 5RHWZUEhnRfs/hsp/fwkXsynu1jcsUX+HuG8FLa2BNheUPtOcgw+vHJcJ8HnJCrc # UWhdFczf8O+pDiyGhVYX+bDDP3GhGS7TmKmGnbZ9N+MpEhWmbiAVPbgkqykSkzyY # Vr15OApZYK8CAwEAAaOCAVwwggFYMB8GA1UdIwQYMBaAFPZ3at0//QET/xahbIIC # L9AKPRQlMB0GA1UdDgQWBBRfWO1MMXqiYUKNUoC6s2GXGaIymzAOBgNVHQ8BAf8E # BAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcDCDAR # BgNVHSAECjAIMAYGBFUdIAAwTAYDVR0fBEUwQzBBoD+gPYY7aHR0cDovL2NybC5z # ZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljVGltZVN0YW1waW5nUm9vdFI0Ni5jcmww # fAYIKwYBBQUHAQEEcDBuMEcGCCsGAQUFBzAChjtodHRwOi8vY3J0LnNlY3RpZ28u # Y29tL1NlY3RpZ29QdWJsaWNUaW1lU3RhbXBpbmdSb290UjQ2LnA3YzAjBggrBgEF # BQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEMBQADggIB # ABLXeyCtDjVYDJ6BHSVY/UwtZ3Svx2ImIfZVVGnGoUaGdltoX4hDskBMZx5NY5L6 # SCcwDMZhHOmbyMhyOVJDwm1yrKYqGDHWzpwVkFJ+996jKKAXyIIaUf5JVKjccev3 # w16mNIUlNTkpJEor7edVJZiRJVCAmWAaHcw9zP0hY3gj+fWp8MbOocI9Zn78xvm9 # XKGBp6rEs9sEiq/pwzvg2/KjXE2yWUQIkms6+yslCRqNXPjEnBnxuUB1fm6bPAV+ # Tsr/Qrd+mOCJemo06ldon4pJFbQd0TQVIMLv5koklInHvyaf6vATJP4DfPtKzSBP # kKlOtyaFTAjD2Nu+di5hErEVVaMqSVbfPzd6kNXOhYm23EWm6N2s2ZHCHVhlUgHa # C4ACMRCgXjYfQEDtYEK54dUwPJXV7icz0rgCzs9VI29DwsjVZFpO4ZIVR33LwXyP # DbYFkLqYmgHjR3tKVkhh9qKV2WCmBuC27pIOx6TYvyqiYbntinmpOqh/QPAnhDge # xKG9GX/n1PggkGi9HCapZp8fRwg8RftwS21Ln61euBG0yONM6noD2XQPrFwpm3Gc # uqJMf0o8LLrFkSLRQNwxPDDkWXhW+gZswbaiie5fd/W2ygcto78XCSPfFWveUOSZ # 5SqK95tBO8aTHmEa4lpJVD7HrTEn9jb1EGvxOb1cnn0CMIIGGjCCBAKgAwIBAgIQ # Yh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG9w0BAQwFADBWMQswCQYDVQQGEwJHQjEY # MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdvIFB1Ymxp # YyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIx # MjM1OTU5WjBUMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVk # MSswKQYDVQQDEyJTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgQ0EgUjM2MIIB # ojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAmyudU/o1P45gBkNqwM/1f/bI # U1MYyM7TbH78WAeVF3llMwsRHgBGRmxDeEDIArCS2VCoVk4Y/8j6stIkmYV5Gej4 # NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk9vT0k2oWJMJjL9G//N523hAm4jF4UjrW # 2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7XwiunD7mBxNtecM6ytIdUlh08T2z7mJEXZ # D9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ0arWZVeffvMr/iiIROSCzKoDmWABDRzV # /UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZXnYvZQgWx/SXiJDRSAolRzZEZquE6cbcH # 747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+tAfiWu01TPhCr9VrkxsHC5qFNxaThTG5j # 4/Kc+ODD2dX/fmBECELcvzUHf9shoFvrn35XGf2RPaNTO2uSZ6n9otv7jElspkfK # 9qEATHZcodp+R4q2OIypxR//YEb3fkDn3UayWW9bAgMBAAGjggFkMIIBYDAfBgNV # HSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaRXBeF5jAdBgNVHQ4EFgQUDyrLIIcouOxv # SK4rVKYpqhekzQwwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw # EwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYDVR0gBBQwEjAGBgRVHSAAMAgGBmeBDAEE # ATBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3Rp # Z29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYuY3JsMHsGCCsGAQUFBwEBBG8wbTBG # BggrBgEFBQcwAoY6aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGlj # Q29kZVNpZ25pbmdSb290UjQ2LnA3YzAjBggrBgEFBQcwAYYXaHR0cDovL29jc3Au # c2VjdGlnby5jb20wDQYJKoZIhvcNAQEMBQADggIBAAb/guF3YzZue6EVIJsT/wT+ # mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXKZDk8+Y1LoNqHrp22AKMGxQtgCivnDHFy # AQ9GXTmlk7MjcgQbDCx6mn7yIawsppWkvfPkKaAQsiqaT9DnMWBHVNIabGqgQSGT # rQWo43MOfsPynhbz2Hyxf5XWKZpRvr3dMapandPfYgoZ8iDL2OR3sYztgJrbG6VZ # 9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwFkvjFV3jS49ZSc4lShKK6BrPTJYs4NG1D # GzmpToTnwoqZ8fAmi2XlZnuchC4NPSZaPATHvNIzt+z1PHo35D/f7j2pO1S8BCys # QDHCbM5Mnomnq5aYcKCsdbh0czchOm8bkinLrYrKpii+Tk7pwL7TjRKLXkomm5D1 # Umds++pip8wH2cQpf93at3VDcOK4N7EwoIJB0kak6pSzEu4I64U6gZs7tS/dGNSl # jf2OSSnRr7KWzq03zl8l75jy+hOds9TWSenLbjBQUGR96cFr6lEUfAIEHVC1L68Y # 1GGxx4/eRI82ut83axHMViw1+sVpbPxg51Tbnio1lB93079WPFnYaOvfGAA0e0zc # fF/M9gXr+korwQTh2Prqooq2bYNMvUoUKD85gnJ+t0smrWrb8dee2CvYZXD5laGt # aAxOfy/VKNmwuWuAh9kcMIIGRzCCBK+gAwIBAgIQacs5SDkvNuif0aEmZmr03jAN # BgkqhkiG9w0BAQwFADBUMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBM # aW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgQ0Eg # UjM2MB4XDTI1MDEyOTAwMDAwMFoXDTI4MDEyOTIzNTk1OVowXjELMAkGA1UEBhMC # VVMxEzARBgNVBAgMCk5ldyBKZXJzZXkxHDAaBgNVBAoME0N5YmVyTWVsIENvbnN1 # bHRpbmcxHDAaBgNVBAMME0N5YmVyTWVsIENvbnN1bHRpbmcwggIiMA0GCSqGSIb3 # DQEBAQUAA4ICDwAwggIKAoICAQDBQmSvdfamF8o0CJr4vbHCcJ4rwx6T1HR3d32u # 4aIf9v9p/GV4nFdG4PP9SMjWw7Nx9CLFqGPpkw7aDU2IxwpfPYExDzkCj2pgiyeV # KlL0itTlPocb6i1cZLe/WHV7aUkGkVlfvyYIqdJ9uw711dhNWmMhlqo+/qyp+gpK # qaiFHm6mWNVg2KLTH5Pu38cBoGhS1tn7mlQbtALNjehkpFw2AAntEIBzM3ZEg9WB # xQlgYY0yAPkydYbJfTEOEFJqHUPTSV46jx22Jb9dl0cEIPsGrCp+Jo5Ugusp9oZE # CZ8bGt7Vc9jYoIWGpqcRDq1JZFNCSVvNE4N3ECGjq6W3kYW7ot0CP1DkpJ93a5wr # ksQ6bvYGUy3lghkMvzjkkq/NVUDEVcdNR7PsUFf654vSw+iLINZ+9kYg+Znplfnd # T/JSMJDAaWkM5oLu6+ao0774QWrsHOttz7M8EDU+3PntYHglwWoej6qXIFRurgXd # wAXXyXYcSmkOTbPqrjSwsbs8CuSwGqebbRSDKfjRzDqQ9D1AZ/JHHaaUkBbAYBsV # MrvypDSrP/1o37mt4Zky28BnEp5ztEGp0HJ44X4rFVWWz+BfeuZWcVUcGKW2YFHo # bNwGmJ/OanLvlnmtpZIRLF9ZkbzCHHomi+RId4g3fc3FsGxKqEW9Vj8PCumwKc6L # UwZU4wIDAQABo4IBiTCCAYUwHwYDVR0jBBgwFoAUDyrLIIcouOxvSK4rVKYpqhek # zQwwHQYDVR0OBBYEFCiCHmEfvPkU1uIc2sPugFDBq88SMA4GA1UdDwEB/wQEAwIH # gDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMEoGA1UdIARDMEEw # NQYMKwYBBAGyMQECAQMCMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5j # b20vQ1BTMAgGBmeBDAEEATBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLnNl # Y3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ0NBUjM2LmNybDB5Bggr # BgEFBQcBAQRtMGswRAYIKwYBBQUHMAKGOGh0dHA6Ly9jcnQuc2VjdGlnby5jb20v # U2VjdGlnb1B1YmxpY0NvZGVTaWduaW5nQ0FSMzYuY3J0MCMGCCsGAQUFBzABhhdo # dHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAYEAmLUUP/C5 # nHN/qX27dIrfNezHdUul/uhOA5CwNkD7P4pvLJButR/S1OmvozuzJJTce6824Iyl # nXkRwUFj04XLbodkBL7+YwQ5ml7CjdDSVo+sI/38jcEQ6FgosV/TTJSiFAgqMNwk # x/kSzvQ1/Ufp5YVKggCXGJ4VitIzl5nMbzzu35G/uy4vmCQfh0KPYUTJYiRsF6Z3 # XJiIVtYrEwN/ikif/WFGrzsFj1OOWHNn5qDOP80xExmRS09z/wdZE9RdjPv5fYLn # KWy1+GQ/w1vzg/l2vUXIgBV0MxalUfTP4V9Spsodrb+noPXiCy5n+6hy9yCf3EQb # 3G1n8rT/a454fLSijMm6bhrgBRqhPUUtn6ZIBdEJzJUI6ftuXrQnB/U7zf32xcTT # AW7WPem7DFK/4JrSaxiXcSkxQ4kXJDVoDPUJdpb0c5XdWVJO0DCkB35ONEIoqT6V # jEIjLPSw9UXE420r1OIpV8FRJqrW4Fr5RUveEUlyF+FyygVOYZECNsjRMIIGYjCC # BMqgAwIBAgIRAKQpO24e3denNAiHrXpOtyQwDQYJKoZIhvcNAQEMBQAwVTELMAkG # A1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDEsMCoGA1UEAxMjU2Vj # dGlnbyBQdWJsaWMgVGltZSBTdGFtcGluZyBDQSBSMzYwHhcNMjUwMzI3MDAwMDAw # WhcNMzYwMzIxMjM1OTU5WjByMQswCQYDVQQGEwJHQjEXMBUGA1UECBMOV2VzdCBZ # b3Jrc2hpcmUxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDEwMC4GA1UEAxMnU2Vj # dGlnbyBQdWJsaWMgVGltZSBTdGFtcGluZyBTaWduZXIgUjM2MIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEA04SV9G6kU3jyPRBLeBIHPNyUgVNnYayfsGOy # YEXrn3+SkDYTLs1crcw/ol2swE1TzB2aR/5JIjKNf75QBha2Ddj+4NEPKDxHEd4d # En7RTWMcTIfm492TW22I8LfH+A7Ehz0/safc6BbsNBzjHTt7FngNfhfJoYOrkugS # aT8F0IzUh6VUwoHdYDpiln9dh0n0m545d5A5tJD92iFAIbKHQWGbCQNYplqpAFas # HBn77OqW37P9BhOASdmjp3IijYiFdcA0WQIe60vzvrk0HG+iVcwVZjz+t5OcXGTc # xqOAzk1frDNZ1aw8nFhGEvG0ktJQknnJZE3D40GofV7O8WzgaAnZmoUn4PCpvH36 # vD4XaAF2CjiPsJWiY/j2xLsJuqx3JtuI4akH0MmGzlBUylhXvdNVXcjAuIEcEQKt # OBR9lU4wXQpISrbOT8ux+96GzBq8TdbhoFcmYaOBZKlwPP7pOp5Mzx/UMhyBA93P # QhiCdPfIVOCINsUY4U23p4KJ3F1HqP3H6Slw3lHACnLilGETXRg5X/Fp8G8qlG5Y # +M49ZEGUp2bneRLZoyHTyynHvFISpefhBCV0KdRZHPcuSL5OAGWnBjAlRtHvsMBr # I3AAA0Tu1oGvPa/4yeeiAyu+9y3SLC98gDVbySnXnkujjhIh+oaatsk/oyf5R2vc # xHahajMCAwEAAaOCAY4wggGKMB8GA1UdIwQYMBaAFF9Y7UwxeqJhQo1SgLqzYZcZ # ojKbMB0GA1UdDgQWBBSIYYyhKjdkgShgoZsx0Iz9LALOTzAOBgNVHQ8BAf8EBAMC # BsAwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDBKBgNVHSAE # QzBBMDUGDCsGAQQBsjEBAgEDCDAlMCMGCCsGAQUFBwIBFhdodHRwczovL3NlY3Rp # Z28uY29tL0NQUzAIBgZngQwBBAIwSgYDVR0fBEMwQTA/oD2gO4Y5aHR0cDovL2Ny # bC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljVGltZVN0YW1waW5nQ0FSMzYuY3Js # MHoGCCsGAQUFBwEBBG4wbDBFBggrBgEFBQcwAoY5aHR0cDovL2NydC5zZWN0aWdv # LmNvbS9TZWN0aWdvUHVibGljVGltZVN0YW1waW5nQ0FSMzYuY3J0MCMGCCsGAQUF # BzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAYEA # AoE+pIZyUSH5ZakuPVKK4eWbzEsTRJOEjbIu6r7vmzXXLpJx4FyGmcqnFZoa1dzx # 3JrUCrdG5b//LfAxOGy9Ph9JtrYChJaVHrusDh9NgYwiGDOhyyJ2zRy3+kdqhwtU # lLCdNjFjakTSE+hkC9F5ty1uxOoQ2ZkfI5WM4WXA3ZHcNHB4V42zi7Jk3ktEnkSd # ViVxM6rduXW0jmmiu71ZpBFZDh7Kdens+PQXPgMqvzodgQJEkxaION5XRCoBxAwW # wiMm2thPDuZTzWp/gUFzi7izCmEt4pE3Kf0MOt3ccgwn4Kl2FIcQaV55nkjv1gOD # cHcD9+ZVjYZoyKTVWb4VqMQy/j8Q3aaYd/jOQ66Fhk3NWbg2tYl5jhQCuIsE55Vg # 4N0DUbEWvXJxtxQQaVR5xzhEI+BjJKzh3TQ026JxHhr2fuJ0mV68AluFr9qshgwS # 5SpN5FFtaSEnAwqZv3IS+mlG50rK7W3qXbWwi4hmpylUfygtYLEdLQukNEX1jiOK # MIIGgjCCBGqgAwIBAgIQNsKwvXwbOuejs902y8l1aDANBgkqhkiG9w0BAQwFADCB # iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl # cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV # BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjEw # MzIyMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjBXMQswCQYDVQQGEwJHQjEYMBYGA1UE # ChMPU2VjdGlnbyBMaW1pdGVkMS4wLAYDVQQDEyVTZWN0aWdvIFB1YmxpYyBUaW1l # IFN0YW1waW5nIFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC # AgEAiJ3YuUVnnR3d6LkmgZpUVMB8SQWbzFoVD9mUEES0QUCBdxSZqdTkdizICFNe # INCSJS+lV1ipnW5ihkQyC0cRLWXUJzodqpnMRs46npiJPHrfLBOifjfhpdXJ2aHH # sPHggGsCi7uE0awqKggE/LkYw3sqaBia67h/3awoqNvGqiFRJ+OTWYmUCO2GAXse # PHi+/JUNAax3kpqstbl3vcTdOGhtKShvZIvjwulRH87rbukNyHGWX5tNK/WABKf+ # Gnoi4cmisS7oSimgHUI0Wn/4elNd40BFdSZ1EwpuddZ+Wr7+Dfo0lcHflm/FDDrO # J3rWqauUP8hsokDoI7D/yUVI9DAE/WK3Jl3C4LKwIpn1mNzMyptRwsXKrop06m7N # UNHdlTDEMovXAIDGAvYynPt5lutv8lZeI5w3MOlCybAZDpK3Dy1MKo+6aEtE9vti # TMzz/o2dYfdP0KWZwZIXbYsTIlg1YIetCpi5s14qiXOpRsKqFKqav9R1R5vj3Nge # vsAsvxsAnI8Oa5s2oy25qhsoBIGo/zi6GpxFj+mOdh35Xn91y72J4RGOJEoqzEIb # W3q0b2iPuWLA911cRxgY5SJYubvjay3nSMbBPPFsyl6mY4/WYucmyS9lo3l7jk27 # MAe145GWxK4O3m3gEFEIkv7kRmefDR7Oe2T1HxAnICQvr9sCAwEAAaOCARYwggES # MB8GA1UdIwQYMBaAFFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBT2d2rd # P/0BE/8WoWyCAi/QCj0UJTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB # /zATBgNVHSUEDDAKBggrBgEFBQcDCDARBgNVHSAECjAIMAYGBFUdIAAwUAYDVR0f # BEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdFJT # QUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMDUGCCsGAQUFBwEBBCkwJzAlBggr # BgEFBQcwAYYZaHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwF # AAOCAgEADr5lQe1oRLjlocXUEYfktzsljOt+2sgXke3Y8UPEooU5y39rAARaAdAx # UeiX1ktLJ3+lgxtoLQhn5cFb3GF2SSZRX8ptQ6IvuD3wz/LNHKpQ5nX8hjsDLRhs # yeIiJsms9yAWnvdYOdEMq1W61KE9JlBkB20XBee6JaXx4UBErc+YuoSb1SxVf7nk # NtUjPfcxuFtrQdRMRi/fInV/AobE8Gw/8yBMQKKaHt5eia8ybT8Y/Ffa6HAJyz9g # vEOcF1VWXG8OMeM7Vy7Bs6mSIkYeYtddU1ux1dQLbEGur18ut97wgGwDiGinCwKP # yFO7ApcmVJOtlw9FVJxw/mL1TbyBns4zOgkaXFnnfzg4qbSvnrwyj1NiurMp4pmA # WjR+Pb/SIduPnmFzbSN/G8reZCL4fvGlvPFk4Uab/JVCSmj59+/mB2Gn6G/UYOy8 # k60mKcmaAZsEVkhOFuoj4we8CYyaR9vd9PGZKSinaZIkvVjbH/3nlLb0a7SBIkiR # zfPfS9T+JesylbHa1LtRV9U/7m0q7Ma2CQ/t392ioOssXW7oKLdOmMBl14suVFBm # bzrt5V5cQPnwtd3UOTpS9oCG+ZZheiIvPgkDmA8FzPsnfXW5qHELB43ET7HHFHeR # PRYrMBKjkb8/IN7Po0d0hQoF4TeMM+zYAJzoKQnVKOLg8pZVPT8xggYwMIIGLAIB # ATBoMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzAp # BgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYCEGnLOUg5 # Lzbon9GhJmZq9N4wCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAIoAKAAKEC # gAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwG # CisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFNTyEbqh0Wwdwnb/MdsmnET7isQ/ # MA0GCSqGSIb3DQEBAQUABIICADeVrp4FgSvIwt+1dzb+U3hVNgfcVXjKBpDn2DYj # aZgdqgtKpg9wPofkMeyKNkpPp8EC+nTzha8pUzm9QWyGOliDtSWEPPXr3n3h5h4G # 9D6FWGaPwY343wN/S2KS0GG6rzfKg0FQ9nq7vaqivMZuyjPyZg3Qo5U5soJoKYfX # bJPDUQ+P01yBGo323F10z/F+dUmPnE/YdmtXh/PvIt8oFVrOfXeCATcbV+aeOGsX # rPfZDFKmltgo2jTutTJg6V19Fm11Ug6s8zcxbpUCxLrngozTVtb/HyBXJbdKdQdc # 36pBy6D9AOD26B5/XBLwu4E+ML2NbjtxPD63V0e5AD6TGd5YmurDI5A2pItPHyP4 # VjNSrKcyRCeMFXFvSlJZgSb9nnEIgsvQmEwq18FgTKhjw+V9C01L+BgN2CT5QI44 # ctBjcxQojODI6oH1M3P6GF0tlEbbkHhO59OWP8ypI3YAp82gBfIiF3BhgDNFk8G9 # DIdlAwHR1X18pkbGu70DDxo3Iyd0etzpusJpw4ur3UutOpITnRXIGRb6jlcFJWtt # 7ApADoId6/mtVxcoXQVYLMcGICXtknq8QC1IJtymKmpywol5SwS2aPVRXvgaMo+2 # PBLOYwGOsHDKKU6h90rGyz0MMBHSla/uq5y4xyOx0zatvkOld94nCYm7lEJEFMFY # KZ7xoYIDIzCCAx8GCSqGSIb3DQEJBjGCAxAwggMMAgEBMGowVTELMAkGA1UEBhMC # R0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDEsMCoGA1UEAxMjU2VjdGlnbyBQ # dWJsaWMgVGltZSBTdGFtcGluZyBDQSBSMzYCEQCkKTtuHt3XpzQIh616TrckMA0G # CWCGSAFlAwQCAgUAoHkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG # 9w0BCQUxDxcNMjUwNzE3MDQ1ODQ2WjA/BgkqhkiG9w0BCQQxMgQwttqaH+GyoeMU # 42W8mP0IPiwUwcq49POK/OMuLc33mFD7FdPl7NmURxdlHvdog2HdMA0GCSqGSIb3 # DQEBAQUABIICAJL3R61j7xtBcibTac70ZfN0Sl5N6HutYHqvITsYnEdLbGE1LoKK # +D4H61sG3KP6q0FR5wK3Hq0ejN4C5yN/5FtLMkP2PFLCQGUVtYB47a1qjbF9eIPq # SZi9k5BpGFHzPW9TascHA0p/utHKbQG2uiIC+fKLtyfNRZzmmHnUQdxJ9qEGIZL4 # gXEF8uphPzP4lLzYw5JBfvJ2gzIRAPUXVNE+++NGbGNDf3LX6cfcOiBS1qXOvS90 # hvE7CEpzzSmgLZfwGyRYI6KmkOMBt3Q/YX3PRWJwGJHV0Hd3CyGFLyzNP++9OE/I # ifKhvpEagXIfMQmH9aA9XR+DJDvpxhfiRVAcORcVXnY66nOnQzzzH06q9Cp7H4Ss # EMAeyyaMctBTkBDYeKjSDtGONldSeAYc054o7HHOP8hdzW0ajVhuoY2DdEldGW3J # qTuUs5KxezLOoj/Bqglwqshq9DbeLylK9qJuZFv5w6t802KTKV+ORBuRY4DtDdRW # yjtZ0xxHnHSJrvK0IxQoU1M/NLlg8cLsUzGRxFwTf/+Mfg1ANB4c8ATVnFn8rzFc # nVavEauXDapfqa+yOph+6rC99+uG/d0SaRnqNI7zkqfPgGdFlK5LD5hW3fQmKq3X # T/KI7ij0lIIKj95S6eofDQnf8rz/Eyt3uJTjLNKDQQMagdNc4cYpJKBg # SIG # End signature block |