Monitoring for Deleted Sources in Lifecycle States

# How to Monitor for Deleted Sources and Remove Them from Lifecycle States

The Problem

If this story sounds familiar then stick around and I’m going to save you from a lot of frustration and, more importantly, wasted time. And if this story doesn’t sound familiar, stick around anyway.

I’ll set the scene for you. You’ve finished onboarding a new source into your IdentityNow tenant. You get your account and entitlement queries dialed in, your Correlation attributes are set, and you even have a few solid roles and access profiles rolled out. The only outstanding item is adding the new source to some lifecycle states.

You know some of the lifecycle states (LCSs) have more than 40 sources listed, so there’s no point in clicking around the GUI. And even if they were all under 40, everyone knows it’s faster, more fun and makes you feel smarter to use the CLI anyway. At least until you see this message:

"text": "The request was syntactically correct but its content is semantically invalid."

The Solution

I won’t lie. Every time I see this error I imagine hitting whoever wrote it with their thesaurus. You just know it was opened to the “S” section when they came up with this. Okay, I’m done ranting. I’ll get back on topic now.

If this is your first time using IdentityNow’s APIs to update an LCS, then it’s possible you have a typo in the JSON body somewhere. I lost hours one day because of a trailing space in a hash table on a key name. Surprise, IdentityNow didn’t like it much.

But with LCSs, this error can also mean you have a source in your Account Actions list that was deleted. Unfortunately, IdentityNow doesn’t remove the IDs for sources when they’re deleted. So it’s up to you to track them down.

Or you can start monitoring and removing these deleted sources on your own. Instead of being surprised by that wordy error message, you can put all those APIs to work for you. I’ll keep this process as vanilla as I can, but I’ll be using my own PowerShell Module available here on GitHub. You can also install it from the PowerShell Gallery.

Now if the idea of automatically removing sources from your LCSs makes you nervous, I understand. Okay, that’s not entirely true, my boss understands, which means I implemented this as a two-step process. I wrote one script to check for deleted sources in our LCSs and another to update each LCS after we confirm that the deleted sources were truly deleted.

The first script exports a JSON file and sends an email summary with the deleted sources’ IDs. After you confirm that these sources are really gone, you can run the second script that imports the JSON, updates the relevant LCSs and creates a JSON backup just in case. Let’s face it. If things didn’t go horribly wrong on occasion, then most of us would be out of work. This approach allows you to adapt the script to fit your environment. I’ll provide the code to create the JSON export and process it. But where it’s saved and how you manage this process will be up to you.

Before we begin

First, you need to authenticate to your IdentityNow Tenant. If this is your first time running the IdnTools Module, then you’ll need to start by running the command Update-IdnConfigSettings to provide the Org Name for your Production and Sandbox Tenants. This command creates a new environmental variable. The scope parameter dictates whether it’s created as a system or user variable.

Next, you’ll need to use a personal access token (PAT) to authenticate to your tenant. All examples will assume that you’re connecting to a production tenant, but if you want to run this in your sandbox, then first add -Instance Sandbox to the end of all commands. Run the following to obtain a bearer token. The Get-Credential command is used for simplicity.

$PAT = Get-Credential
Get-IdnToken -ClientId $PAT.UserName -ClientSecret $PAT.Password

The reason you’re still reading

Time for the good part. First, you need to get a list of all your identity profiles and a complete list of sources.

$Profiles = Get-IdnIdentityProfiles 
$Sources  = Get-IdnSources

Next, you’ll need to run a foreach loop against those profiles. This loop will retrieve all the LCSs for the current profile and filter out all states with no account actions. It’ll start two more foreach loops. The first loop is against all LCSs with account actions, and the second is against each action itself. It compares the source IDs in the current action against the list of all sources that was pulled earlier. If there are any source IDs found in the actions list that aren’t in the source list, a PSCustomObject is created, detailing the updates needed for the current action.

$Result = foreach ($IdnProfile in $Profiles) {

    $States = Get-IdnLifeCycleStates -ProfileId $
    $Audit  = $States | Where-Object { $_.accountActions.Count -ge 1 }

    $Result = foreach ($LifeCycle in $Audit) {

        foreach ($Action in $LifeCycle.accountActions) {

            $Compare    = Compare-Object -ReferenceObject $Action.sourceIds -DifferenceObject $
            $Removals   = @( $Compare | Where-Object { $_.SideIndicator -eq "<=" } )

            if ( $Removals.Count -ge 1 ) {

                [System.Collections.ArrayList]$UpdateList = $Action.sourceIds

                foreach ($Cleanup in $Removals.InputObject) {

                    $UpdateList.Remove( $Cleanup )

                    IdentityProfile = $
                    ProfileID       = $
                    LifeCycle       = $
                    LifecycleID     = $
                    SourcesBefore   = $Action.sourceIds.Count
                    ActiontoTake    = $Action.action
                    SourcesAfter    = $UpdateList.Count
                    SourcesRemoved  = $Removals.InputObject
                    OriginalList    = $Action.sourceIds
                    UpdateList      = $UpdateList



Still with me? Great! I promise we’re almost done. If none of your LCSs contained a deleted source ID, then you’re done. If not, then the way you proceed will depend on how you ran the code. If you’re running this script manually you can skip this part. If it’s running automatically through a CI/CD pipeline or as a scheduled task, then I’d recommend exporting the results to JSON and sending an email alerting you and your team that deleted sources were found. This is some sample code used to export the JSON and format a table to send as an email:

$Json = ConvertTo-Json -InputObject $Result -Depth 10
foreach ($Change in $Result) {
    $Change.SourcesRemoved = $Change.SourcesRemoved -join "-LINEBREAK-"


$Header = "<style>
TABLE {border-width: 1px; border-style: solid; border-color: black; border-collapse: collapse;}
TH {border-width: 1px; padding: 3px; border-style: solid; border-color: black; background-color: #6495ED;}
TD {border-width: 1px; padding: 3px; border-style: solid; border-color: black;}

$Html = $Result | Select-Object "IdentityProfile","LifeCycle","SourcesBefore","SourcesAfter","SourcesRemoved" | ConvertTo-Html -Head $Header -As "Table"
$Edit = $Html -Creplace "-LINEBREAK-" , "<br>"

Taking out the trash

We’re in the Endgame now. If you ran the first part manually, you can pick up where you left off with the $Result variable. If not, you’ll need to recreate using the JSON exported earlier.

A word of warning before you proceed. We don’t have any LCSs with multiple account actions. So if one of your LCSs contains both an enable and a disable action, you’ll need to make some changes to the rest of the code I provide. Otherwise you’ll remove one of the actions from your settings.

First, create empty arrays to back up the current LCSs settings and another empty array to save the updates to. After you have done this, it’s a much simpler foreach loop to grab a backup of each LCS you’re updating, build the body for the update command and send it off. Once you’ve completed this process, the backup and update arrays are converted to JSON. I’ll admit I use the update to keep VS Code from yelling at me for an unused variable, but the backup may save you from a Resume Creating Event if something goes horribly wrong.

$Backup     = @()
$Updated    = @()

foreach ($Update in $Result) {

    $Params    = @{ 
        ProfileId           = $Update.ProfileID 
        LifeCycleStateExtId = $Update.LifecycleID


    $Backup += Get-IdnLifeCycleState @Params

    $BodyParam = switch ($Update.ActiontoTake) {

        "ENABLE"    { @{ EnableSourceIdList     = $Update.UpdateList } }
        "DISABLE"   { @{ DisableSourceIdList    = $Update.UpdateList } }    

    $PatchBody = Write-IdnLifeCycleJsonPatch @BodyParam
    $Updated  += Update-IdnLifeCycleState @Params -PatchObject $PatchBody


$BackupJSON = $Backup | ConvertTo-Json -Depth 10
$UpdateJSON = $Update | ConvertTo-Json -Depth 10

That’s all folks

Thanks for hanging in with me. I know I threw a lot at you. But my team has dealt with this a couple times recently. Hopefully this helps take one annoying task off your To Do List.