scripts/Invoke-IdentityNowAccountCorrelation.ps1

function Invoke-IdentityNowAccountCorrelation {
    <#
    .SYNOPSIS
        Find uncorrelated accounts that can be joined
 
    .DESCRIPTION
        Compare identities to a source's uncorrelated accounts to see if there are un-joined accounts which would benefit from an unoptimized aggregation or manual correlation csv upload
 
    .PARAMETER org
        string, optional, the name of your org if you wish to switch, calls Set-IdentityNowOrg
 
    .PARAMETER sourceName
        string, required, the name of the source like "Corporate Active Directory", "ServiceNow", "AAD"
 
    .PARAMETER identityAttribute
        string, required, the system name of the identity attribute which will be tested for a match against accountAttribute
 
    .PARAMETER accountAttribute
        string, required, the account attribute that should equal the value of identityAttribute, it could be userprincipalname, employeeid, or any other unique value
 
    .PARAMETER missingAccountQuery
        string, optional, the search query used to identify identities that are missing an account
        the default will be "NOT @accounts(source.name:`"$sourcename`")"
        in large environments, providing stricter criteria like, we also expect an account in AAD, or certain attributes should have a value, or only for this identity profile, can speed up the search query
        IDN has a limit of 10,000 on their search, you may need to break up the identity results if necessary.
 
    .PARAMETER limit
        integer, batch size for fetching identities and accounts for IDN API, default is 250
     
    .PARAMETER triggerJoin
        switch, after outputting joins will upload csv to IDN to manually correlate identities to accounts
 
    .EXAMPLE
        Invoke-IdentityNowAccountCorrelation -sourceName "Prod AAD" -identityAttribute calculatedImmuteableID -accountAttribute immuteableId
 
    .EXAMPLE
        Invoke-IdentityNowAccountCorrelation -sourceName "HR" -identityAttribute identificationNumber -accountAttribute EmployeeID -triggerJoin -limit 500
 
    .LINK
        http://darrenjrobinson.com/sailpoint-identitynow
 
    #>

    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$org,
        [Parameter(Mandatory = $true)]
        [string]$sourceName,
        [Parameter(Mandatory = $true)]
        [string]$identityAttribute,
        [Parameter(Mandatory = $true)]
        [string]$accountAttribute,
        [string]$missingAccountQuery = "NOT @accounts(source.name:`"$sourcename`") AND attributes.$($identityAttribute):*",
        [ValidateRange(0, 250)]
        [int]$limit = 250,
        [switch]$triggerJoin
    )
    if ($org) { 
        Set-IdentityNowOrg $org 
    }

    try {
        $org = (Get-IdentityNowOrg).'Organisation Name'
    }
    catch {
        throw "Possibly missing SailpointIdentityNow PowerShell module: $_"
    }
    
    $searchBody = [pscustomobject]@{
        indices = @("identities")
        query   = [pscustomobject]@{
            query  = $missingAccountQuery
            fields = @("name", "description")
        }
    }
    $source = Get-IdentityNowSource
    $source = $source.where{ $_.name -eq $sourcename }[0]
    $auth = Get-IdentityNowAuth -return V3JWT
    $i = 0
    $accounts = @()
    write-output "Getting from beta accounts API 'sourceId eq `"$($source.externalId)`" and uncorrelated eq true'"

    do {
        $url = "https://$org.api.identitynow.com/beta/accounts?count=true&limit=$limit&offset=$($limit*$i)&filters=sourceId eq `"$($source.externalId)`" and uncorrelated eq true"
        try {
            $temp = Invoke-RestMethod -UseBasicParsing `
                -Uri $url `
                -Headers @{"Authorization" = "Bearer $($auth.access_token)" } `
                -Method Get
        }
        catch {
            switch ($_.Exception.Response.StatusCode) {
                'GatewayTimeout' { Write-Error "$($_.Exception.Response.StatusCode):$_" }
                default { "$($_.Exception.Response.StatusCode):$_" }
            }
        }

        if ($temp.count -eq 1) { 
            $temp = ConvertFrom-Json ($temp -creplace '\"ImmutableId\"\:(null|\"[\w\d\\\+\-\@\.\/]{1,}\"),', '') 
        }

        $accounts += $temp
        $i++
        write-progress -activity 'get accounts' -status $accounts.Count
    } until ($temp.count -lt $limit)

    write-output "retrieved $($accounts.count)"
    $auth = Get-IdentityNowAuth -return V3JWT
    $i = 0
    $missingaccount = @()
    write-output "getting Identities from v3 search API:$missingAccountQuery"
    do {
        $url = "https://$org.api.identitynow.com/v3/search?count=true&limit=$limit&offset=$($limit*$i)"
        $temp = $null
        $temp = Invoke-RestMethod -UseBasicParsing `
            -Uri $url `
            -Headers @{"Authorization" = "Bearer $($auth.access_token)" } `
            -Method Post `
            -Body ($searchBody | ConvertTo-Json) `
            -ContentType 'application/json'
        
        if ($temp.count -ge 1) { 
            $missingaccount += $temp 
        }
        if ($temp.Count -eq $limit) { 
            $i++ 
        }
        write-progress -activity 'get identities' -status $missingaccount.Count
    } until ($temp.count -lt $limit)

    write-output "retrieved $($missingAccount.count) identities"
    $i = 0
    $joins = @()
    foreach ($user in $missingaccount) {
        $i++
        if ($user.attributes.$identityAttribute -in $accounts.attributes.$accountAttribute) {
            $joins += [pscustomobject]@{
                account     = $accounts.where{ $_.attributes.$accountAttribute -eq $user.attributes.$identityAttribute }.nativeIdentity
                displayName = $accounts.where{ $_.attributes.$accountAttribute -eq $user.attributes.$identityAttribute }.nativeIdentity
                userName    = $user.name
                type        = $null
            }
            write-output $joins[-1] | ConvertTo-Json
        }
    }

    if ($triggerJoin -and $joins.count -ge 1) {
        $joins | Join-IdentityNowAccount -org $org -source $source.id
    }
}
# SIG # Begin signature block
# MIINSwYJKoZIhvcNAQcCoIINPDCCDTgCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUy17btNg888z9+ePtOkFhk4FX
# NdegggqNMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1b5VQCDANBgkqhkiG9w0B
# AQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk
# IElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgxMDIyMTIwMDAwWjByMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQg
# Q29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
# +NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLXcep2nQUut4/6kkPApfmJ
# 1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSRI5aQd4L5oYQjZhJUM1B0
# sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXiTWAYvqrEsq5wMWYzcT6s
# cKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5Ng2Q7+S1TqSp6moKq4Tz
# rGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8vYWxYoNzQYIH5DiLanMg
# 0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYDVR0TAQH/BAgwBgEB/wIB
# ADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwMweQYIKwYBBQUH
# AQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYI
# KwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz
# c3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaG
# NGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RD
# QS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0
# dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZIAYb9bAMwHQYDVR0OBBYE
# FFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6en
# IZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPzItEVyCx8JSl2qB1dHC06
# GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRupY5a4l4kgU4QpO4/cY5j
# DhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKNJK4kxscnKqEpKBo6cSgC
# PC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmifz0DLQESlE/DmZAwlCEIy
# sjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN3fYBIM6ZMWM9CBoYs4Gb
# T8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKyZqHnGKSaZFHvMIIFVTCC
# BD2gAwIBAgIQDOzRdXezgbkTF+1Qo8ZgrzANBgkqhkiG9w0BAQsFADByMQswCQYD
# VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGln
# aWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29k
# ZSBTaWduaW5nIENBMB4XDTIwMDYxNDAwMDAwMFoXDTIzMDYxOTEyMDAwMFowgZEx
# CzAJBgNVBAYTAkFVMRgwFgYDVQQIEw9OZXcgU291dGggV2FsZXMxFDASBgNVBAcT
# C0NoZXJyeWJyb29rMRowGAYDVQQKExFEYXJyZW4gSiBSb2JpbnNvbjEaMBgGA1UE
# CxMRRGFycmVuIEogUm9iaW5zb24xGjAYBgNVBAMTEURhcnJlbiBKIFJvYmluc29u
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwj7PLmjkknFA0MIbRPwc
# T1JwU/xUZ6UFMy6AUyltGEigMVGxFEXoVybjQXwI9hhpzDh2gdxL3W8V5dTXyzqN
# 8LUXa6NODjIzh+egJf/fkXOgzWOPD5fToL7mm4JWofuaAwv2DmI2UtgvQGwRhkUx
# Y3hh0+MNDSyz28cqExf8H6mTTcuafgu/Nt4A0ddjr1hYBHU4g51ZJ96YcRsvMZSu
# 8qycBUNEp8/EZJxBUmqCp7mKi72jojkhu+6ujOPi2xgG8IWE6GqlmuMVhRSUvF7F
# 9PreiwPtGim92RG9Rsn8kg1tkxX/1dUYbjOIgXOmE1FAo/QU6nKVioJMNpNsVEBz
# /QIDAQABo4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1Dlgw
# HQYDVR0OBBYEFOh6QLkkiXXHi1nqeGozeiSEHADoMA4GA1UdDwEB/wQEAwIHgDAT
# BgNVHSUEDDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0
# cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYD
# VR0gBEUwQzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cu
# ZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElE
# Q29kZVNpZ25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOC
# AQEANWoHDjN7Hg9QrOaZx0V8MK4c4nkYBeFDCYAyP/SqwYeAtKPA7F72mvmJV6E3
# YZnilv8b+YvZpFTZrw98GtwCnuQjcIj3OZMfepQuwV1n3S6GO3o30xpKGu6h0d4L
# rJkIbmVvi3RZr7U8ruHqnI4TgbYaCWKdwfLb/CUffaUsRX7BOguFRnYShwJmZAzI
# mgBx2r2vWcZePlKH/k7kupUAWSY8PF8O+lvdwzVPSVDW+PoTqfI4q9au/0U77UN0
# Fq/ohMyQ/CUX731xeC6Rb5TjlmDhdthFP3Iho1FX0GIu55Py5x84qW+Ou+OytQcA
# FZx22DA8dAUbS3P7OIPamcU68TGCAigwggIkAgEBMIGGMHIxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25p
# bmcgQ0ECEAzs0XV3s4G5ExftUKPGYK8wCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcC
# AQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYB
# BAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFMMEGHjD92r3
# OByEed00RXIb61XgMA0GCSqGSIb3DQEBAQUABIIBAF3iQIOPTIv+hN9zL5X8cVSl
# 303sawO416f/u3Qf870GbvHUJNJ9dPkv5FWHZHYaSi2dIZUoNFh71VfHV+0Dj0hZ
# qQGqt1cs25Ix0+fJItoAgDmLOMG7s7Zoklo+iowRqRBtODZEgB0sayzC+6fjTeCz
# jfRYQEYmYkgJ1FUfmYjB0OEKdIGxl0tmc8a6Mew4sUx6NrmIGTCzBITdDgb+QqyM
# 1DIFwTcu9mDhNuF+4jJnD6Hnito+noJSiUGLdW8SxCdWHWRUDoRcK/+nQtApH57D
# FU8RA9+cEjJwRT61q+JWEw2Ulo0+Ns8Vva4fgEUJ0bAqtCqWbs3zjzAcfaGEcrM=
# SIG # End signature block