Transform - Find Users' Last Activity Date (find latest date in array)

Iterating on a post I made about 6 weeks ago with a different way to achieve the same goal!

I wanted to expand on my previous transform (which looks at the AD LastLogon and LastLogonTimestamp attributes, as well as the AzureAD/EntraID lastSignInDateTime and lastNonInteractiveSignInDateTime attributes), to also pull in the AD pwdLastSet attribute as well as the AD whenCreated attribute. Because comparing 6 values using if statements would be cumbersome and even uglier than comparing 4 values was, I decided to dive back into Velocity to try and figure out a better way.

Now, through the magic of arrays and foreach loops, Iā€™ve found one!

Iā€™ve simplified my previous transform a bit (by removing the essentially duplicated values to maintain the to-the-minute accuracy) while also adding the two additional attributes, and am now returning null values rather than error when a value couldnā€™t be determined.

The transform:

{
    "name": "mc-LastActiveDate",
    "type": "firstValid",
    "attributes": {
        "values": [
            {
                "type": "dateFormat",
                "attributes": {
                    "input": {
                        "type": "static",
                        "attributes": {
                            "adLastLogon": {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "firstValid",
                                        "attributes": {
                                            "values": [
                                                {
                                                    "type": "accountAttribute",
                                                    "attributes": {
                                                        "attributeName": "lastLogon",
                                                        "sourceName": "Active Directory"
                                                    }
                                                },
                                                {
                                                    "attributes": {
                                                        "value": "125911584000000000"
                                                    },
                                                    "type": "static"
                                                }
                                            ]
                                        }
                                    },
                                    "inputFormat": "EPOCH_TIME_WIN32",
                                    "outputFormat": "yyMMddHH"
                                }
                            },
                            "adLastLogonTimestamp": {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "firstValid",
                                        "attributes": {
                                            "values": [
                                                {
                                                    "type": "accountAttribute",
                                                    "attributes": {
                                                        "attributeName": "lastLogonTimestamp",
                                                        "sourceName": "Active Directory"
                                                    }
                                                },
                                                {
                                                    "attributes": {
                                                        "value": "125911584000000000"
                                                    },
                                                    "type": "static"
                                                }
                                            ]
                                        }
                                    },
                                    "inputFormat": "EPOCH_TIME_WIN32",
                                    "outputFormat": "yyMMddHH"
                                }
                            },
                            "adPwdLastSet": {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "firstValid",
                                        "attributes": {
                                            "values": [
                                                {
                                                    "type": "accountAttribute",
                                                    "attributes": {
                                                        "attributeName": "pwdLastSet",
                                                        "sourceName": "Active Directory"
                                                    }
                                                },
                                                {
                                                    "attributes": {
                                                        "value": "125911584000000000"
                                                    },
                                                    "type": "static"
                                                }
                                            ]
                                        }
                                    },
                                    "inputFormat": "EPOCH_TIME_WIN32",
                                    "outputFormat": "yyMMddHH"
                                }
                            },
                            "adWhenCreated": {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "firstValid",
                                        "attributes": {
                                            "values": [
                                                {
                                                    "type": "accountAttribute",
                                                    "attributes": {
                                                        "attributeName": "whenCreated",
                                                        "sourceName": "Active Directory"
                                                    }
                                                },
                                                {
                                                    "attributes": {
                                                        "value": "20000101010000.0Z"
                                                    },
                                                    "type": "static"
                                                }
                                            ]
                                        }
                                    },
                                    "inputFormat": "yyyyMMddHHmmss.S'Z'",
                                    "outputFormat": "yyMMddHH"
                                }
                            },
                            "azureLastInteractive": {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "firstValid",
                                        "attributes": {
                                            "values": [
                                                {
                                                    "type": "accountAttribute",
                                                    "attributes": {
                                                        "attributeName": "lastSignInDateTime",
                                                        "sourceName": "Azure Active Directory"
                                                    }
                                                },
                                                {
                                                    "attributes": {
                                                        "value": "2000-01-01T00:00:00Z"
                                                    },
                                                    "type": "static"
                                                }
                                            ]
                                        }
                                    },
                                    "inputFormat": "yyyy-MM-dd'T'HH:mm:ss'Z'",
                                    "outputFormat": "yyMMddHH"
                                }
                            },
                            "azureLastNonInteractive": {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "firstValid",
                                        "attributes": {
                                            "values": [
                                                {
                                                    "type": "accountAttribute",
                                                    "attributes": {
                                                        "attributeName": "lastNonInteractiveSignInDateTime",
                                                        "sourceName": "Azure Active Directory"
                                                    }
                                                },
                                                {
                                                    "attributes": {
                                                        "value": "2000-01-01T00:00:00Z"
                                                    },
                                                    "type": "static"
                                                }
                                            ]
                                        }
                                    },
                                    "inputFormat": "yyyy-MM-dd'T'HH:mm:ss'Z'",
                                    "outputFormat": "yyMMddHH"
                                }
                            },
                            "value": "#set($max=-10)#set($array = [$max.parseInt($adLastLogon),$max.parseInt($adLastLogonTimestamp),$max.parseInt($adPwdLastSet),$max.parseInt($adWhenCreated),$max.parseInt($azureLastInteractive),$max.parseInt($azureLastNonInteractive)])#foreach($val in $array)#if($val > $max)#set($max = $val)#end#end#if($max > 0)$max#end"
                        }
                    },
                    "inputFormat": "yyMMddHH",
                    "outputFormat": "ISO8601"
                }
            },
            null
        ]
    },
    "internal": false
}

The main logic can be broken down as follows. Remember we have to parse the dates as integers in order for the comparisons to work:

#set($max=-10) ## Instantiate a variable as an int; this will be our base for comparison as well as an int that will help us parse the dates as integers so we can compare them
#set($array = [$max.parseInt($adLastLogon),$max.parseInt($adLastLogonTimestamp),$max.parseInt($adPwdLastSet),$max.parseInt($adWhenCreated),$max.parseInt($azureLastInteractive),$max.parseInt($azureLastNonInteractive)]) ## Build an array of integers representing our dates
#foreach($val in $array) ## Iterate through the array
    #if($val > $max) ## Compare each value in the array against the current max value (which started at -10)
        #set($max = $val) ## If the current value is greater than the max, that value becomes the new max
    #end
#end
#if($max > 0) ## If the max value is greater than 0, return the max value
    $max
#end

Shout-out to @tyler_mairose and @brennenscott for various bits of inspiration!

3 Likes

@mcorsillo_bose Iā€™ve been playing around with Apache velocity myself and this is a pretty great use case of what it possible! Good stuff!

1 Like

Is any of this going to be used on Azure guest accounts? Iā€™ve had a hard time relying on lastNonInteractiveSignInDateTime to indicate actual activity.

This is more of an example of using some Velocity functionality thatā€™s not included in the transform documentation, like arrays and foreach and a way to compare multiple dates.

That said, Iā€™m only planning on using this as a way to generate a list of accounts for our operations team to look into. Eventually, we may use it to automatically trigger a recertification, but in the short term theyā€™ll be reaching out to managers to verify whether the folks are still around.

Yeah, I was more asking outside the technical implementation of this.

We have a hard time validating whether or not guest accounts in Azure are still needed or even active, and have found that using the lastNonInteractiveSignInDateTime is not a reliable way of determining that. For a guest account, just populating their tenant picker in an application can trigger an update of that field, which is frustrating.

1 Like

Ah, good to know! I donā€™t believe weā€™re using guest accounts, so shouldnā€™t have that issue, but at least this is erring on the side of showing more recent activity rather than less recent activity? :stuck_out_tongue:

Totally non-technical, but about ā€œComplianceā€ - If we assume the IGA system is ā€œmanaging identities and associated accounts to a ā€˜desiredā€™ stateā€, then the last login becomes a bit irrelevant in the majority of cases Iā€™ve seen where people want this value. The most prevalent use case is ā€œDisable inactive accountsā€, but this is generally in conflict with ā€œdesired stateā€ which provides the account in the first place. This begs the questions of - Is the desired state correctly configured? Is someone not using the account that they should be using? etc.

@edmarks while I donā€™t disagree with you, itā€™s important to remember that different orgs and industries have different use cases. While thereā€™s obviously an ideal world in which the HRIS platform is our Source of Truth and always has accurate and up-to-date information, and our non-employees are properly managed in a system where their access is properly shut down by managers when their engagements/contracts/terms end, the world in which we live and work is far from ideal :slight_smile:

To be clear, Iā€™m not suggesting automation to trigger deprovisioning when a userā€™s last activity date is outside some specific threshold - Iā€™m using this particular bit of logic just to identify users who likely havenā€™t been active in a while, and our operations team will then reach out to the managers/HR to verify that the users should still be active. This will help to catch cases where managers didnā€™t properly enter terms into Workday or work with HR to do so, or didnā€™t end contracts for contingent workers who are no longer on our projects. From a security perspective, of course, this still leaves a huge gap, as it wouldnā€™t alert us to folks who are still actively logging in even after they shouldnā€™t be, so itā€™s not a full solution.

That said, thereā€™s plenty of cases where itā€™s encouraged to unlicense folks from certain applications when theyā€™re no longer using said applications, or remove their access when itā€™s clearly no longer being used. Thereā€™s entire companies whose products exist specifically for this sort of functionality, and ISC offers this sort of functionality itself as well as part of the Access Insights, so the last activity date data is not wholly irrelevant in an IGA platform.

But again, this post isnā€™t meant to be a debate on using the last activity date or its relevance to SailPoint/IGA in general. This is simply a Show And Tell post showing a way to leverage Velocity in a way that was otherwise not documented elsewhere in SailPoint docs or the forum, to effectively compare mulitple dates, and hopefully inspire folks to look into other ways to extend the documented functionality we can get from Transforms by diving deeper into the Velocity scripting language.

1 Like

@mcorsillo_bose Weā€™re aligned and youā€™re spot on that different orgs have different requirements and intentions on how to use the data collected.

1 Like