|
function Get-JcLoginEvent |
|
{ |
|
[CmdletBinding()] |
|
param ( |
|
[Parameter(HelpMessage = "Set to true to print the number of users who would have been suspend, this will not suspend users")] |
|
[switch] |
|
$ReadOnly = $false, |
|
[Parameter(Mandatory = $true, HelpMessage = "Path to the CSV file. If no such CSV file exists, it will be created. If the CSV file does exist, it will be updated.")] |
|
[string] |
|
$CsvPath, |
|
[Parameter(Mandatory = $true, HelpMessage = "The DateTime value to begin filtering between. ex. (Get-Date.AddDays(-30)) ")] |
|
[datetime] |
|
$StartDate, |
|
[Parameter(Mandatory = $false, HelpMessage = "The DateTime value to end filtering between. Default: Get-Date (current date). ex. (Get-Date.AddDays(-30)) ")] |
|
[datetime] |
|
$EndDate |
|
) |
|
begin |
|
{ |
|
# Write date of last execution to a file |
|
$CsvDirectory = Split-Path $CsvPath |
|
if ( [System.String]::IsNullOrEmpty($EndDate) ) |
|
{ |
|
$EndDate = Get-Date |
|
} |
|
New-Item -Path $CsvDirectory -Name "jc-login-suspend-last-run.txt" -Value $EndDate -Force |
|
|
|
# Check if CSV exists. If it does not, create an empty one. |
|
If (Test-Path -Path $CsvPath -PathType Leaf) |
|
{ |
|
Write-Debug "File found at location: $($CsvPath)." |
|
[System.Collections.ArrayList]$CsvData = Import-Csv -Path $CsvPath |
|
} |
|
Else |
|
{ |
|
Write-Debug "No file found at location: $($CsvPath). Creating file." |
|
[System.Collections.ArrayList]$CsvData = @() |
|
} |
|
|
|
$UserList = Get-JcSdkUser |
|
|
|
$DaysPerSpan = .5 |
|
$Span = New-TimeSpan -Start $StartDate -End $EndDate |
|
Write-Debug "Quering Events between $StartDate and $EndDate" |
|
|
|
$Spans = @() |
|
for ($i = 0; $i -lt $Span.Days; $i += ($DaysPerSpan)) |
|
{ |
|
$LoopStart = ($StartDate).AddDays($i) |
|
$LoopEnd = ($StartDate).AddDays($i + $DaysPerSpan) |
|
If ($LoopEnd -gt $EndDate) |
|
{ |
|
$LoopEnd = $EndDate |
|
} |
|
$Spans += New-Object psobject -Property @{StartDate = $LoopStart; EndDate = $LoopEnd } |
|
} |
|
Write-Debug "$($Spans.Count) day interval(s) will be queried" |
|
} |
|
process |
|
{ |
|
# Gather Directory Insights Data |
|
$EventTypes = ( |
|
"radius_auth", |
|
"sso_auth", |
|
"login_attempt", |
|
"ldap_bind", |
|
"user_login_attempt" |
|
) |
|
$FilterFields = @( |
|
"success", |
|
"initiated_by", |
|
"event_type", |
|
"timestamp" |
|
) |
|
Write-Debug "Collecting Directory Insights data for the following event types: $($EventTypes). This may take a moment." |
|
#$DirectoryInsightsData = Get-JcSdkEvent -Service all -StartTime $StartDate -SearchTermOr @{ "event_type" = $EventTypes } |
|
|
|
$stopwatch = [system.diagnostics.stopwatch]::StartNew() |
|
$ObjectJobs = @() |
|
foreach ($span in $spans) |
|
{ |
|
$Count = Get-JCEventCount -Service:('all') -StartTime:($($span.StartDate)) -endTime:($($span.EndDate)) -SearchTermOr @{ "event_type" = $EventTypes } |
|
if ($Count -ge 1) |
|
{ |
|
Write-Host "Returning $Count events between: $($span.StartDate) - $($span.EndDate)" |
|
$ObjectJobs += start-ThreadJob -ScriptBlock:( { Param ($EventTypes, $span, $FilterFields) |
|
$ErrorActionPreference = 'Continue'; |
|
$Result = Get-JCSDKEvent -Service:('all') -StartTime:($($span.StartDate)) -endTime:($($span.EndDate)) -SearchTermOr @{ "event_type" = $EventTypes } -Fields:($FilterFields) |
|
Return $Result |
|
}) -ArgumentList:($EventTypes, $span, $FilterFields) -StreamingHost:($Host) -ThrottleLimit:(30) |
|
} |
|
} |
|
$ObjectJobStatus = Wait-Job -Id:($ObjectJobs.Id) |
|
if ('Failed' -in $ObjectJobStatus.State) |
|
{ |
|
# if failures in the jobs report & exit: |
|
Write-Error "failed to query all data from Directory Insights" |
|
} |
|
$DirectoryInsightsData += $ObjectJobStatus | Where-Object { $_.state -eq 'Completed' } | Receive-Job |
|
$stopwatch.Stop() |
|
$totalSecs = [math]::Round($stopwatch.Elapsed.TotalSeconds, 0) |
|
Write-host "Events query took $totalSecs seconds to run" |
|
|
|
ForEach ( $Event in $DirectoryInsightsData | Where-Object { ($_.success -eq 'true') -or ($Event.event_type -eq 'sso_auth') } ) |
|
{ |
|
# Check if the Event Type is "login_attempt". These events do not have the "username" as an initiated_by attribute and id must be used instead. |
|
If ( $Event.event_type -in "login_attempt", "ldap_bind" ) |
|
{ |
|
# if username in current userlist |
|
If ( $Event.initiated_by.username -in $UserList.username ) |
|
{ |
|
# idIndex returns username from $event |
|
$IdIndex = $UserList.username.IndexOf($Event.initiated_by.username) |
|
# if event username matches existing username already in csv |
|
If ( $Event.initiated_by.username -in $CsvData.username ) |
|
{ |
|
# returns username from $event |
|
$RowIndex = $CsvData.username.IndexOf($Event.initiated_by.username) |
|
# update the timestamp for this user (latest auth) |
|
$CsvData[$RowIndex].timestamp = $Event.timestamp |
|
} |
|
else |
|
{ |
|
# if new user to the csv |
|
$CsvData.Add( |
|
[PSCustomObject]@{ |
|
id = $UserList[$IdIndex].id; |
|
username = $Event.initiated_by.username; |
|
timestamp = $Event.timestamp |
|
} |
|
) | Out-Null |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
If ( $Event.initiated_by.id -in $CsvData.id ) |
|
{ |
|
$RowIndex = $CsvData.id.IndexOf($Event.initiated_by.id) |
|
$CsvData[$RowIndex].timestamp = $event.timestamp |
|
} |
|
else |
|
{ |
|
$CsvData.Add( |
|
[PSCustomObject]@{ |
|
id = $Event.initiated_by.id; |
|
username = $Event.initiated_by.username; |
|
timestamp = $Event.timestamp |
|
} |
|
) | Out-Null |
|
} |
|
} |
|
} |
|
ForEach ($user in $UserList){ |
|
# Add users to the list with NULL Timestamps if they were not found above |
|
If ($user.id -notin $CsvData.id){ |
|
$CsvData.Add( |
|
[PSCustomObject]@{ |
|
id = $user.id; |
|
username = $user.username; |
|
timestamp = $NULL |
|
} |
|
) | Out-Null |
|
} |
|
} |
|
} |
|
end |
|
{ |
|
if ( $ReadOnly ) |
|
{ |
|
$CsvData |
|
} |
|
else |
|
{ |
|
$CsvData | Export-Csv $CsvPath |
|
} |
|
} |
|
} |
|
|
|
function Suspend-InactiveUsers |
|
{ |
|
[CmdletBinding()] |
|
param ( |
|
[Parameter(HelpMessage = "Set to true to print the number of users who would have been suspend, this will not suspend users")] |
|
[switch] |
|
$ReadOnly = $false, |
|
[Parameter(Mandatory = $true, HelpMessage = "Path to the CSV file. If no such CSV file exists, it will be created. If the CSV file does exist, it will be updated.")] |
|
[string] |
|
$CsvPath, |
|
[Parameter(HelpMessage = "Custom Attribute to Filter against, if the value of this custom attribute is true, this user will be excluded from suspension")] |
|
[string] |
|
$ExcludeAttribute, |
|
[Parameter(Mandatory = $true, HelpMessage = "The integer value to filter between the current date ex. (30) ")] |
|
[int] |
|
$DaysSinceLogin |
|
) |
|
begin |
|
{ |
|
$SuspendedList = [System.Collections.ArrayList]@() |
|
$CsvDirectory = Split-Path $CsvPath |
|
$LastRunTimePath = $CsvDirectory + "/jc-login-suspend-last-run.txt" |
|
If ( Test-Path -Path $LastRunTimePath -PathType Leaf ) |
|
{ |
|
$LastRunTime = ( Get-Date ( Get-Content $LastRunTimePath ) ) |
|
$SuspendDate = $LastRunTime.AddDays(-$DaysSinceLogin) |
|
Write-Debug "Searching for user's who's last known login time occured before: $SuspendDate" |
|
} |
|
else |
|
{ |
|
Write-Error "ERROR: Unable to locate file at location: $($LastRunTimePath)." |
|
} |
|
|
|
# Check if CSV exists. If it does not, error out. |
|
If (Test-Path -Path $CsvPath -PathType Leaf) |
|
{ |
|
Write-Debug "File found at location: $($CsvPath)." |
|
[System.Collections.ArrayList]$CsvData = Import-Csv -Path $CsvPath |
|
} |
|
Else |
|
{ |
|
Write-Error "ERROR: Unable to locate CSV at location: $($CsvPath)." |
|
} |
|
|
|
$UserList = Get-JcSdkUser |
|
if ($ExcludeAttribute) |
|
{ |
|
$exclusionUsers = @() |
|
foreach ($user in $UserList) |
|
{ |
|
if ($user.Attributes | Where-Object { $_.Name -eq $ExcludeAttribute -and $_.Value -eq 'true' }) |
|
{ |
|
# if user has custom attribute name w/ true value add it to the exclusion list. |
|
$exclusionUsers += $user |
|
} |
|
} |
|
Write-Debug "Users found: $($UserList.count)" |
|
Write-Debug "Exclusion Users found: $($exclusionUsers.count)" |
|
$difference = $UserList | where-object { $_.username -notcontains $exclusionUsers.username } |
|
Write-Debug "Users w/o $ExcludeAttribute attribute found: $($difference.count)" |
|
# just set the users object to difference... |
|
$UserList = $difference |
|
} |
|
} |
|
process |
|
{ |
|
ForEach ( $User in $CsvData ) |
|
{ |
|
# If the user has been deleted since the last run, remove them from the CSV |
|
If ( ($User.id -notin $UserList.id) -And ($User.id -notin $exclusionUsers.id ) ) |
|
{ |
|
Write-Debug "$($User.username) has been deleted. Removing from list." |
|
If (-not $ReadOnly) |
|
{ |
|
$CsvData.RemoveAt($CsvData.id.IndexOf($User.id)) |
|
} |
|
} |
|
else |
|
{ |
|
If ([string]::IsNullOrEmpty(($User.timestamp))){ |
|
# User has a null timestamp |
|
Write-Debug "Suspending user: $($User.username). Last login: N/A." |
|
$SuspendedUser = Get-JcSdkUser -Id $User.Id |
|
$SuspendedList.Add([PSCustomObject]@{ |
|
Username = $SuspendedUser.Username; |
|
Firstname = $SuspendedUser.Firstname; |
|
Lastname = $SuspendedUser.Lastname; |
|
Email = $SuspendedUser.Email; |
|
LastLoginDate = $User.timestamp; |
|
Id = $SuspendedUser.Id; |
|
Manager = ( $SuspendedUser.Attributes | ? { $_.Name -eq "Manager" } ).Value; |
|
}) | Out-Null |
|
If ( -not $ReadOnly ) |
|
{ |
|
Set-JcSdkUser -Id $User.id -State "SUSPENDED" | Out-Null |
|
$CsvData.RemoveAt($CsvData.id.IndexOf($User.id)) |
|
} |
|
} |
|
ElseIf (([datetime]$User.timestamp).ticks -lt $SuspendDate.ticks) |
|
{ |
|
# Else the user has a valid timestamp, compare with the suspend date |
|
Write-Debug "Suspending user: $($User.username). Last login: $($User.timestamp)." |
|
$SuspendedUser = Get-JcSdkUser -Id $User.Id |
|
$SuspendedList.Add([PSCustomObject]@{ |
|
Username = $SuspendedUser.Username; |
|
Firstname = $SuspendedUser.Firstname; |
|
Lastname = $SuspendedUser.Lastname; |
|
Email = $SuspendedUser.Email; |
|
LastLoginDate = $User.timestamp; |
|
Id = $SuspendedUser.Id; |
|
Manager = ( $SuspendedUser.Attributes | ? { $_.Name -eq "Manager" } ).Value; |
|
}) | Out-Null |
|
If ( -not $ReadOnly ) |
|
{ |
|
Set-JcSdkUser -Id $User.id -State "SUSPENDED" | Out-Null |
|
$CsvData.RemoveAt($CsvData.id.IndexOf($User.id)) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
end |
|
{ |
|
if ( -not $ReadOnly ) |
|
{ |
|
$CsvData | Export-Csv $CsvPath |
|
} |
|
$SuspendedList |
|
} |
|
} |
Strong work, Joe. One question, would it possibly make more sense for the disable parameters to be $true and $dryrun? Having a parameter named $false, makes me think we're actually doing something there.