public/Invoke-VPASMetricsPSM.ps1
<#
.Synopsis RUN VARIOUS PSM METRICS FROM CYBERARK CREATED BY: Vadim Melamed, EMAIL: vmelamed5@gmail.com .DESCRIPTION USE THIS FUNCTION TO GENERATE VARIOUS PSM RELATED METRICS FROM CYBERARK .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 HTMLChart Specify the HTML report type Possible values: BarGraph, LineGraph, PieChart, ALL .PARAMETER HideRawData Removes the RawData visual from the exported output Helpful when exporting to a PDF or document to remove extra not needed information .EXAMPLE $GenerateReport = Invoke-VPASMetricsPSM -TargetMetric PSMSessionsInXDays -OutputDirectory "C:\temp\VPASMetrics" -MetricFormat ALL -HTMLChart ALL -DayRange 7 -AmtOfSets 8 .OUTPUTS HashTable object if successful $false if failed #> function Invoke-VPASMetricsPSM{ [OutputType([bool])] [CmdletBinding()] Param( [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,HelpMessage="Enter TargetMetric to be generated (PSMSessionsInXDays,PSMUtilizationForXDays,PSMConnectionComponentsInXDays,UsersConnectingWithPSMInXDays)",Position=0)] [ValidateSet('PSMSessionsInXDays','PSMUtilizationForXDays','PSMConnectionComponentsInXDays','UsersConnectingWithPSMInXDays')] [String]$TargetMetric, [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,HelpMessage="Enter ReportOutput type (JSON, HTML, ALL, NONE)",Position=1)] [ValidateSet('JSON','HTML','ALL','NONE')] [String]$MetricFormat, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=2)] [String]$OutputDirectory, [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,HelpMessage="Specify the date range for the selected metric report",Position=3)] [String]$DayRange, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=4)] [String]$AmtOfSets, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=5)] [ValidateSet('BarGraph','LineGraph','PieChart','ALL')] [String]$HTMLChart, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=6)] [String]$AmtOfUsers, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=7)] [switch]$HideRawData, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=8)] [hashtable]$token ) Begin{ $tokenval,$sessionval,$PVWA,$Header,$ISPSS,$IdentityURL,$EnableTextRecorder,$AuditTimeStamp,$NoSSL,$VaultVersion,$HideWarnings,$AuthenticatedAs,$SubDomain = Get-VPASSession -token $token $CommandName = $MyInvocation.MyCommand.Name $log = Write-VPASTextRecorder -inputval $CommandName -token $token -LogType COMMAND } Process{ Write-Verbose "SUCCESSFULLY PARSED PVWA VALUE" Write-Verbose "SUCCESSFULLY PARSED TOKEN VALUE" Write-Verbose "SUCCESSFULLY PARSED TARGET METRIC VALUE: $TargetMetric" Write-Verbose "SUCCESSFULLY PARSED METRIC OUTPUT VALUE: $MetricFormat" try{ 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 = "BarGraph" } 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){ $EpochStart = $rec.Start $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){ $EpochStart = $rec.Start $TargetPSM = $rec.RawProperties.ProviderID 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){ $EpochStart = $rec.Start $TargetCC = $rec.ConnectionComponentID 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){ $EpochStart = $rec.Start $TargetUser = $rec.User 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 } } } $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; } </style> </head> <script src=`"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js`"></script> <body> <div style=`"max-width: 1200px; width: 100%; margin: 0 auto;`"> <div style=`"width: 95%; margin-right: 1%; margin-left: 1%`" class=`"metrics-container3`"> $titlesplit <small><small>(Powered By Vpas)</small></small> </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> <div class=`"metric`"> <span class=`"metric-label`">Output Table(s):</span> $tablestr </div> </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> " | Set-content $outputfile if($HTMLChart -eq "BarGraph" -or $HTMLChart -eq "ALL"){ write-output "<div style=`"max-width:95%; margin-right: 1%; margin-left: 1%`"class=`"metrics-container`"><canvas height=`"500%`" id=`"myChartBAR`" style=`"width:100%;`"></canvas></div>`n<br><br>`n" | Add-Content $outputfile } if($HTMLChart -eq "LineGraph" -or $HTMLChart -eq "ALL"){ write-output "<div style=`"max-width:95%; margin-right: 1%; margin-left: 1%`"class=`"metrics-container`"><canvas height=`"500%`" id=`"myChartLINE`" style=`"width:100%;`"></canvas></div>`n<br><br>`n"| Add-Content $outputfile } if($HTMLChart -eq "PieChart" -or $HTMLChart -eq "ALL"){ write-output "<div style=`"max-width:95%; margin-right: 1%; margin-left: 1%`"class=`"metrics-container`"><canvas height=`"500%`" id=`"myChartPIE`" style=`"width:100%;`"></canvas></div>`n<br><br>`n"| Add-Content $outputfile } Write-Output " <div style=`"display: flex;`"> <div style=`"width:33%; margin-right: 1%; margin-left: 1%`" class=`"metrics-container`"> <div class=`"metric`"> <span class=`"metric-label`">Totals:</span> </div> " | 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 Write-Output " <div class=`"metric`"> <span class=`"metric-label`"><small> Set$i) $Max-$Min`:</small></span><small> $curCount $tagout</small> </div> " | Add-Content $outputfile $i += 1 } } else{ foreach($key in $datahash.Keys){ $curCount = $datahash."$key".count Write-Output " <div class=`"metric`"> <span class=`"metric-label`"><small> $key`:</small></span><small> $curCount $tagout</small> </div> " | Add-Content $outputfile } } Write-Output " </div> <div style=`"max-width:58%; width: 68%; margin-right: 1%; margin-left: 1%`" 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> </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> " | Add-Content $outputfile if($HTMLChart -eq "BarGraph" -or $HTMLChart -eq "ALL"){ write-output "const xValuesBAR = [$tempstr];" | Add-Content $outputfile write-output "const yValuesBAR = [$tempstr2];" | Add-Content $outputfile write-output "const barColorsBAR = [$tempstr3];" | Add-Content $outputfile } if($HTMLChart -eq "LineGraph" -or $HTMLChart -eq "ALL"){ write-output "const xValuesLINE = [$tempstr];" | Add-Content $outputfile write-output "const yValuesLINE = [$tempstr2];" | Add-Content $outputfile } if($HTMLChart -eq "PieChart" -or $HTMLChart -eq "ALL"){ write-output "const xValuesPIE = [$tempstr];" | Add-Content $outputfile write-output "const yValuesPIE = [$tempstr2];" | Add-Content $outputfile write-output "const barColorsPIE = [$tempstr4];" | Add-Content $outputfile } if($HTMLChart -eq "BarGraph" -or $HTMLChart -eq "ALL"){ #BAR Write-Output " new Chart(`"myChartBAR`", { type: `"bar`", data: { labels: xValuesBAR, datasets: [{ backgroundColor: barColorsBAR, data: yValuesBAR }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: true } }] }, legend: {display: false}, title: { display: true, text: `"$titlesplit`" } } }); " | Add-Content $outputfile } if($HTMLChart -eq "LineGraph" -or $HTMLChart -eq "ALL"){ #LINE Write-Output " new Chart(`"myChartLINE`", { type: `"line`", data: { labels: xValuesLINE, datasets: [{ label: `"$titlesplit`", fill: false, lineTension: 0, backgroundColor: `"rgba(0,0,255,1.0)`", borderColor: `"rgba(0,0,255,0.1)`", data: yValuesLINE }] }, options: { legend: {display: true}, scales: { yAxes: [{ticks: {min: $mintick, max:$maxtick}}], } } }); " | Add-Content $outputfile } if($HTMLChart -eq "PieChart" -or $HTMLChart -eq "ALL"){ #PIE write-output " new Chart(`"myChartPIE`", { type: `"pie`", data: { labels: xValuesPIE, datasets: [{ backgroundColor: barColorsPIE, data: yValuesPIE }] }, options: { title: { display: true, text: `"$titlesplit`" } } }); 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`"); } " | 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 } } |