Tuesday, July 20, 2021

Delinquent KnowBe4 training list - API seriously / sadly lacking

If any of you folks use KnowBe4...
The below script may be helpful...
I tapped into their API - Hoping to pull in an automated list of users who have not completed mandatory training -

This list of users was being manually harvested, via the web interface (not an easy, intuitive, or remotely contiguous process either).

Only to discover that such base level, rudimentary details are not actually available from a simple API call...
I even asked their support team... Nope!


To say the least, I was dismayed, and annoyed in learning this.


(I did promise KnowBe4 support that I would call them out on Reddit, hoping that some amount of public embarrassment, might get their dev team to fix " this shortsighted absurdity"... " And act like a company that offers IT related services" - 
https://www.reddit.com/r/PowerShell/comments/oo9ir1/delinquent_knowbe4_training_list_api_seriously/

https://www.reddit.com/r/sysadmin/comments/oo9n51/delinquent_knowbe4_training_list_api_seriously/

I sent a link to those Reddit posts, in my reply to the KnowBe4 manager that replied to me, when I explained my feelings about this, in the support email)

~~~~~~~~~~~~~~~~~~~~~~~~~~
It is necessary to call three (3) different API directories, in order to get something useful.
Querying first ‘Campaigns’, and then using THAT result, and querying ‘Enrollments’, and THEN using THAT result, querying ‘users’.

The below code does a far bit of data massaging, and may be interspersed with some filtering that not everyone might need in place...
No doubt there are ways to streamline it - TBH - I don't really care.

Much of what I did, was done on-the-fly... And like I said - I was annoyed... I probably still am.


$EAP = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue" # Because possibly dealing with converting a null value to 'DateTime', below.

Function Token {
$AccessToken = "'Reporting' API key goes in here" # Get this from within the KnowBe4 portal
$authorizationHeader = "Authorization: Bearer $AccessToken"   
}

$URL = 'https://us.api.knowbe4.com/v1/training/campaigns/'
Token
$campaigns = $null; $campaigns = (curl.exe --url $URL --header $authorizationHeader --silent | ConvertFrom-Json)
# $campaigns | select name, campaign_id, status, completion_percentage, start_date, end_date, relative_duration, duration_type | sort completion_percentage | ft

$campaigns | % {
If ($_.start_date -ne $null) { $_.start_date = [DateTime]"$($_.start_date)" }
If ($_.start_date -eq $null) { $_.start_date = [Nullable[DateTime]]$null }
If ($_.end_date -ne $null) { $_.end_date = [DateTime]"$($_.end_date)" }
If ($_.end_date -eq $null) { $_.end_date = [Nullable[DateTime]]$null }
}

$PastDue = @()
$campaigns | ? { $_.Status -ne "closed" -and ($_.end_date -eq $null -or (Get-Date) -lt $_.end_date) } | sort name  | % {

    Write-Host $_.campaign_id -Fore 14 -No
    Write-Host " - "$_.name -Fore 11 -No
    If ($_.relative_duration) { 
        $_relative_duration = $_.relative_duration
        Write-Host " - Rel. Dur.:"$_.relative_duration -No 
    }
    If (!$_.relative_duration) { 
    
    }
    If ($_.end_date -ne $null) { 
        Write-Host " (End date: $($_.end_date))" -Fore 10 
    }
    If ($_.end_date -eq $null) { 
        Write-Host " (No End date)" 
    }
#>
$URL = "https://us.api.knowbe4.com/v1/training/enrollments?campaign_id=$($_.campaign_id)?per_page=500"
Token
$enrollments = $null; $enrollments = (curl.exe --url $URL --header $authorizationHeader --silent | ConvertFrom-Json)
$enrollments = $enrollments | select campaign_name, completion_date, content_type, enrollment_date, enrollment_id, module_name, policy_acknowledged, start_date, status, user_status, user, id, first_name, last_name, email
$enrollments | % {
$_.id = $_.user.id
$_.first_name = $_.user.first_name
$_.last_name = $_.user.last_name
$_.email = $_.user.email
}

$PastDue += $enrollments | ? { $_.status -eq "past due" } | select campaign_name, enrollment_date, @{ Name='duration';Expression ={ $_relative_duration } }, days_late, module_name, status, user_status, id, first_name, last_name, email } 
$ErrorActionPreference = $EAP

$PastDue | sort campaign_name, email  | % {
$duration_period = (($_.duration).Trim()).Split(' ')
If ($duration_period[1] -match "weeks")  { $_days = ((Get-Date).AddDays(-([int]$duration_period[0] *7)) - [Datetime]"$($_.enrollment_date)").Days } 
If ($duration_period[1] -match "months") { $_days = ((Get-Date).AddMonths(-([int]$duration_period[0])) - [Datetime]"$($_.enrollment_date)").Days }
$_.days_late = "  $_days "
$_.enrollment_date = [DateTime]"$($_.enrollment_date)"
}
# Use the user 'Active', or 'Archived'?
$PastDue | % { $StatusURL = "https://us.api.knowbe4.com/v1/users/$($_.id)"; Token; $_.user_status = (curl.exe --url $StatusURL --header $authorizationHeader --silent | ConvertFrom-Json).Status }

''; $PastDue | ? {$_.user_status -eq "Active" -and $_.campaign_name -NOTmatch "The.Inside.Man"} | sort campaign_name, email | select campaign_name,duration,days_late,status,user_status,id,first_name,last_name,email | ft

Thursday, July 1, 2021

User info Function

I call this function into both Console, and ISE profiles.
It will pull in the contents of the clipboard, and pull in pertinent user info.
For me - The users location is really helpful, but It will also show if a users account is locked, the password, or the account is expired, etc...

If the clipboard contents don't work, it will rerun the function and prompt...



Function Office {

$EAP = $ErrorActionPreference 
$ErrorActionPreference = "SilentlyContinue"
$Who = Get-Clipboard

if ($Who -eq "x") {Write-Host " - Exiting..."; break}
If ($Who -is [array]) {
$Who =  [string]::Join(" ",$Who)
}
If (!$Who -or $Who.Length -gt 30) {
Write-Host "Full name, username, or email address? " -NoNewline -ForegroundColor Yellow -BackgroundColor DarkMagenta
Write-Host "('X' to exit)" -NoNewline -ForegroundColor Cyan -BackgroundColor DarkMagenta
Write-Host ":" -NoNewline -ForegroundColor Yellow -BackgroundColor DarkMagenta; Write-Host " " -NoNewline
$Who = (Read-Host).Trim()
}
If ($Who -eq "x") {break}
If ($Who) {$Who = "$(($Who).Trim())"}

If (!$Who) {Write-host "Nothing to look for / Nothing entered." -ForegroundColor Yellow -BackgroundColor Black; break}
$Quality = 0
If ($Who -NOTmatch " " -and $Who -NOTmatch "@") { $Quality = 1; Write-Host "Looking for this username: $Who" -ForegroundColor Cyan }
If ($Who -match "@") { $Quality = 2; Write-Host "Looking for this email address: $Who" -ForegroundColor Cyan }
If ($Who -match " ") { $Quality = 3; Write-Host "Looking for this name: $Who" -ForegroundColor Cyan }

Function ADU1 {
$ErrorActionPreference = "SilentlyContinue"
Get-ADUser -filter * -Properties City, State, 
telephoneNumber, mail, Title, Manager, Description, 
PasswordLastSet, LastLogonDate, PasswordExpired, PasswordNeverExpires, 
lockoutTime, LockedOut, AccountExpirationDate
$ErrorActionPreference = "Continue"
} 
$Output = $null
If ($Quality -eq 1) { $Output = ADU1 | ? {$_.SamAccountName -match $Who} }
If ($Quality -eq 2) { $Output = ADU1 | ? {$_.mail -match $Who} }
If ($Quality -eq 3) { $Output = ADU1 | ? {$_.Name -match $Who} }

If (!$Output) {Write-host "`'$Who`' is not valid." -ForegroundColor Yellow -BackgroundColor Black
Set-Clipboard $null
office
}

$Pwd_Age = "$( ( (Get-Date)-($Output.PasswordLastSet) ).Days ) day(s), $(((Get-Date)-($Output.PasswordLastSet) ).Hours) hour(s), $(((Get-Date)-($Output.PasswordLastSet) ).Minutes) minute(s) ago"

$lockoutTime = $Output.lockoutTime
If ($lockoutTime -gt 0) {$lockoutTime = [datetime]::FromFileTime($($lockoutTime))}

$Output = $Output | Select Name, `
@{Name='Username';Expression ={($_.SamAccountName).ToUpper()}}, `
@{Name='Desc';Expression ={(($_.Description).Replace('|','-'))}},`
@{Name='Location';expression={"$($Output.City), $($Output.State)"}},` `
@{Name='Phone';Expression={$("{0:# ###-###-####}" -f [int64](($_.telephoneNumber).Trim('+')))}}, `
Mail, Title, `
@{Name='Manager';Expression ={"$((Get-ADUser ((get-aduser ($_.SamAccountName) -Properties Manager).Manager) | Select `
    @{name="Manager";expression={"$($_.Name) ($(($_.SamAccountName).ToUpper())) - $($_.UserPrincipalName)"}}).Manager)"}}, `
@{name="Last Logon";expression={$_.LastLogonDate}},`
@{name="Lockout time";expression={$lockoutTime}},` 
@{name="Locked out";expression={$_.LockedOut}},` 
@{name="Pwd No Exp";expression={$_.PasswordNeverExpires}},`
@{name="Expiration";expression={$_.AccountExpirationDate}},` 
@{name="Pwd Last Set";expression={$_.PasswordLastSet}},` 
@{name="Pwd Age";expression={$Pwd_Age}},` 
@{name="Pwd Expired";expression={$_.PasswordExpired}}

Write-Host " "

($Output | Out-String).Split("`n|`r",[System.StringSplitOptions]::RemoveEmptyEntries)  | % {
If ($_ -like "Name *") {Write-Host "   $_ ($($Output.Username))"}
If ($_ -like "Location *" ) { Write-Host "     $(($_).Replace('  :',':'))" -ForegroundColor Green }
If ($_ -like "Desc *" -and $Output.Desc -ne $null) {Write-Host "   $_"}
If ($_ -like "Phone *" -and $Output.Phone -ne $null) {Write-Host "   $_"}
If ($_ -like "Mail *") {Write-Host "   $_"}
If ($_ -like "Title *") {Write-Host "   $_"}
If ($_ -like "Manager *") {Write-Host "   $_"}
If ($_ -like "Last Logon *") {Write-Host "   $_"}
If ($_ -like "Expiration *" -and $Output.Expiration -ne $null) { Write-Host "   " -No; Write-Host "$_"  -Fore Red -Back Yellow }
If ($_ -like "Locked out *" -and $Output."Lockout time" -gt 0) {Write-Host "   $_"}
If ($_ -like "Last Logon *" -and $Output."Lockout time" -gt 0) {Write-Host "   $_"}
If ($_ -like "Pwd Last Set *" -and $Output."Pwd No Exp" -ne $True) {Write-Host "   $_" -No}
If ($_ -like "Pwd Expired *" -and $Output."Pwd Expired" -eq $True) {Write-Host " <EXPIRED> " -Fore Red -Back Yellow -No}
If ($_ -like "Pwd Age *" -and $Output."Pwd No Exp" -ne $True) {Write-Host " [$($Output.'Pwd Age')]" -Fore Yellow}
}

If ($host.name -eq "ConsoleHost") {''}
Set-Clipboard $null
$ErrorActionPreference = $EAP 

} # END Function Office {

set-alias -name ofc -value Office