This discussion has been locked. The information referenced herein may be inaccurate due to age, software updates, or external references.
You can no longer post new replies to this discussion. If you have a similar question you can start a new discussion in this forum.

More efficient Account Status Application Monitor

One of our previous admins created a pair of Application Monitors to check the lockout status and the time between password changes for a few of our service\development accounts. One of the application monitors uses a PowerShell script to query Active Directory for both the date and number of days until the password needs to be changed, while the other application monitor uses a similar PowerShell script to check whether or not the account is locked out, and generate a 0/1 statistic for threshold purposes. They generate the desired values, but I was hoping to make things more efficient. First, I'd like to have one application monitor instead of two, and secondly, I'd like to make how the data is stored more efficient.

As they currently function, teach application monitor creates two separate lines on the "APM_DynamicEvidenceCurrent" table, each line has a value for either the 'NumericData' or the 'StringData' column, and places a NULL value in the other column. My desired goal would be to reduce this so that I have one application monitor, running one PowerShell script, and have it create few lines in the table, one line for the password expiration check and one line for the lockout status, populating both the 'NumericData' and 'StringData' column of each. So far, I have the following PowerShell script, which does produce all four desired values in a single script, however, it is still creating four separate lines instead of the desired two.

NOTE: "testanddevops" is not the real account name, it's used here for posting purposes.

$AccountName = "`testanddevops";
$account = Get-ADUser -identity $AccountName -Properties *
$expires = Get-ADUser -identity 'testanddevops'-Properties pwdLastSet| select @{name ="pwdExpires"; expression={$([datetime]::FromFileTime($_.pwdLastSet)).AddDays(60)}}
$ExpirationDate = ($expires.pwdExpires).ToLongDateString();
$TodaysDate = (get-date);
$TimeSpanRaw = New-TimeSpan -start $TodaysDate -end $ExpirationDate;
$ExpirationLeadTime = $TimeSpanRaw.days;
write-host $AccountName
write-host "Message.DaysUntilPasswordExpiration : $ExpirationDate" `n"Statistic.DaysUntilPasswordExpiration : $ExpirationLeadTime";
if
($account.lockedout -eq $False) {write-host Message.LockoutStatus:False `nStatistic.LockoutStatus:0}
Else {(write-host Message.LockoutStatus:True `nStatistic.LockoutStatus:1)}

  • Before we even get to the script, I want to address the elephant database in the room.  That table you reference: APM_DynamicEvidenceCurrent is behaving as it should.  It's designed and indexed in such a way that it's supposed to have the nulls in there.

    In short - no you cannot combine these two distinct statistics into a single line.  But what you can do is make a single call against AD and extract out both pieces of data.


    I went a little mad here...

    # How many days in advance should we check for password expiration?
    $DaysInAdvance = 60
    
    # Use the same "date" for all calls
    $Today = Get-Date
    
    # Assume all accounts NOT locked out (used for the exit code)
    $AnAccountIsLocked = $false
    
    
    #region Define Exit Codes
    # Sourced from: https://blog.kmsigma.com/2017/09/08/sam-powershell-scripting-template/
    $ExitCode = @{
        "Up" = 0;
        "Down" = 1;
        "Warning" = 2;
        "Critical" = 3;
        "Unknown" = 4
    }
    #endregion Define Exit Codes
    
    if ( $args.length -eq 0 ) {
        # No arguments passed into the script - using the 'testing' account
        # wrapping it in @( ... ) so that it's understood to be an array
        $AccountNames = @( "`testanddevops" )
    }
    elseif ( $args.length -gt 5 ) {
        Write-Warning -Message "SolarWinds Component Templates can only return 10 total component statistics (2 components * 5 accounts = 10).  You have provided $( $args.Length ) accounts to check.  Reduce this number and try again."
        break
    }
    else {
        # Store the passed arguments as account names
        $AccountNames = $args
    }
    
    # cycle through each entry in the accounts list provided by the arguments
    ForEach ( $AccountName in $AccountNames ) {
        
        # I don't think we actually need to pull /everything/ in the properties.  Streamlining this list of properties /could/ increase efficiency on slow links
        # We might be able to get away with just:
        # $AdAccount = Get-ADUser -Identity $AccountName -Properties Identity, SamAccountName, pwdLastSet, LockedOut
        $AdAccount = Get-ADUser -Identity $AccountName -Properties *
        
        # This is sort of advanced PowerShell stuff
        # Take an existing object ( $AdAccount ) and extend it with a new property - this one defined by a script
        $AdAccount | Add-Member -MemberType ScriptProperty -Name PasswordExpirationDate -Value { $( [datetime]::FromFileTime( $this.pwdLastSet ) ).AddDays($DaysInAdvance) } -Force
    
        # This is the part you'll need to test for yourself.  I can't remember if "identity" is a good output.  You may need to swap out for 'sAmAccountName'
    
        # First Metric: How many days until the account's password expires
        Write-Host "Message.Expiration_$( $AdAccount.Identity ): Password for '$( $AdAccount.Identity )' expires in $( ( $AdAccount.PasswordExpirationDate - $Today ).Days ) day(s)"
        Write-Host "Statistic.Expiration_$( $AdAccount.Identity ): $( ( $AdAccount.PasswordExpirationDate - $Today ).Days )"
    
        # Second Metric: Is the account currently locked out
        Write-Host "Message.Locked_$( $AdAccount.Identity ): Account for '$( $AdAccount.Identity )' $( if ( $AdAccount.LockedOut ) { "Locked Out" } else { "Not Locked Out" } )"
        Write-Host "Statistic.Locked_$( $AdAccount.Identity ): Account for '$( $AdAccount.Identity )' $( if ( $AdAccount.LockedOut ) { "1" } else { "0" } )"
    
        if ( $AdAccount.LockedOut ) {
            $AnAccountIsLocked = $true
        }
    
    } # Move on to the next $AccountName in the $AccountNames array
    # You still need an exit code for this and I'm going to go with the simplest option.  If ANY of the accounts are locked out, throw "error"
    if ( $AnAccountIsLocked ) {
        exit $ExitCode["Critical"]
    }
    else {
        exit $ExitCode["Up"]
    }
    

    Comments are everywhere, so I hope you can follow along.

    For me, I wanted to look to be as efficient as possible.  That means check as many accounts as possible on a single call and return all the details.  Those names should be put in the script arguments field separated by commas [Details here]:

    The script should cycle through each account name from that list (up to five because you can only have 10 total metrics returned in a single component) and determine the time until next password and if the account is locked out.

    Again: I do not have an Active Directory infrastructure in my home lab from which to pull this information, so I'm going entirely from memory.

  • Thank you for your reply. I did try your script and it partially worked. When used with a single account, it returned all the information desired for the account. However, when I tried multiple accounts, it would only return information for the one that was locked out.

    Unfortunately, I think the thing defeating my efforts is going to be the table design, thank you for explaining that the table is intentionally set the way that it is.

    The overall end goal was to fix a widget the previous admin had created to display the information regarding the password expiration as well as the lockout status. The widget had been working up until the 2023.2.1 release depricated the DynamicEvidence table. In my efforts to fix the problem I saw that they had been using two application monitors, one for the password expiration data, and the other for the lockout status; each of which creates two rows for a total of four in the table. It didn't make much sense to me that they would be using four separate rows to generate this data when it looked like it could use only two. With your explaination that the table is designed that way, I now see why they were using two separate monitors.

  • Is it a custom query widget?  Can you share the code?  (Use an INSERT > CODE element and select 'SQL' as the type)

    This is why we request suggest that people use SWQL instead of SQL for custom elements.  SWQL doesn't change between versions because it's an abstraction layer above the database.

    I'm sure there's a way to get what you want into a better format.

    Ok - also, give a doodle (or insert a table if you prefer) on how you'd want the data presented when final.  It really does help.

  • Should be fine to post the SWQL\SQL expression and a modified screen shot of what the custom widget looked like. I've edited out the account names and such so it will look like a blank table, but enough that you should get the idea.

    Here is the expression that was used by our previous admin, 

    SELECT 
    C.Application.Name AS ApplicationName, 
    C.ComponentID AS ComponentID, 
    C.Name AS ComponentName,  
    C.ApplicationID AS ApplicationID,
    
    CASE WHEN DE.AvgNumericData <= 4 THEN CONCAT('<font color = red ><b>', DE.AvgNumericData,'</b></font>') WHEN DE.AvgNumericData <= 7 THEN CONCAT('<font color = #FFCC00><b>', DE.AvgNumericData,'</b></font>') ELSE CONCAT('<font color = green>', DE.AvgNumericData,'</font>') END AS DaysToExpiration,
    DE.ExpirationDate,
    CASE WHEN LO.LockOutStatus = '1' THEN CONCAT('<font color = red ><b>', 'Locked','</b></font>') WHEN LO.LockOutStatus = '0' THEN CONCAT('<font color = green>', 'Unlocked','</font>') ELSE  CONCAT('<font color = #FFCC00><b>', 'Unknown','</b></font>') END AS LockOutStatus
    
    FROM Orion.APM.Component C
    
    JOIN (SELECT 
    ComponentStatusID,
    MAX(AvgNumericData) AS AvgNumericData,
    MAX(C.Application.Name) AS ApplicationName, 
    MAX(C.ComponentID) AS ComponentID, 
    MAX(C.Name) AS ComponentName,  
    MAX(DE.StringData) AS ExpirationDate, 
    MAX(C.ApplicationID) AS ApplicationID
    FROM Orion.APM.Component C
    JOIN Orion.APM.DynamicEvidence DE
    ON DE.ComponentStatusID = C.CurrentStatus.ComponentStatusID
    WHERE C.Application.Name LIKE 'Service Account Password Expiration Monitor'
    GROUP BY ComponentStatusID) DE
    ON C.Name = DE.ComponentName
    
    JOIN (SELECT 
    ComponentStatusID,
    MAX(C.Application.Name) AS ApplicationName, 
    MAX(C.ComponentID) AS ComponentID, 
    MAX(C.Name) AS ComponentName,  
    MAX(DE.StringData) AS LockOutStatus, 
    MAX(C.ApplicationID) AS ApplicationID
    FROM Orion.APM.Component C
    JOIN Orion.APM.DynamicEvidence DE
    ON DE.ComponentStatusID = C.CurrentStatus.ComponentStatusID
    WHERE C.Application.Name LIKE 'Service Account Lockout Monitor'
    GROUP BY ComponentStatusID) LO
    ON C.Name = LO.ComponentName
    
    WHERE C.Application.Name LIKE 'Service Account Password Expiration Monitor'
    
    ORDER BY DaysToExpiration

  • After learning that the APM.DynamicEvidence table was being deprecated and slated for removal, I started looking at some of the other tables and came up with the following SQL expression

    SELECT
        APM_Component.Name,
        MAX(APM_DynamicEvidence_Current.NumericData) AS 'Days Remaining',
        MAX(APM_DynamicEvidence_Current.StringData) AS 'Last Reset'
    FROM
        APM_DynamicEvidence_Current
    JOIN APM_Component ON APM_Component.ID = APM_DynamicEvidence_Current.ComponentID
    WHERE APM_Component.ApplicationID=319
    GROUP BY APM_Component.ID, APM_Component.Name
    ORDER BY APM_Component.Name

    This seems to work as far as that it will show the date that the password was last reset and how many days between now and the expiration date. However, I do get a warning message that "SQL data source doesn't respect account limitations. Data returned by specified SQL query can be displayed to any user." I also noticed that SWQL Studio doesn't like that expression because of the NULL values being eliminated by aggregate or other SET operation.

    Of course, there's also the fact that this expression doesn't pull the lockout status information from the table, nor does it color code the lockout status either. Which, I'm guessing from my almost non-existent understanding of SQL is because in order to do so, it would have to determine the difference between the password reset values and the lockout status values, something that my expression can't do when both sets of values are associated with the same ApplicationID. (At least, that's what I think is the issue). As best as I can interpret, the previous admin's expression worked because they were using two separate Application Monitors, so there were two different ApplicationIDs to call.

  • Which, I'm guessing from my almost non-existent understanding of SQL is because in order to do so, it would have to determine the difference between the password reset values and the lockout status values, something that my expression can't do when both sets of values are associated with the same ApplicationID. (At least, that's what I think is the issue).

    You are right on the money there.  Which is another of the reasons we recommend the SolarWinds Query Language (SWQL).for this - the object names and relationships are a little more logical. <-- and here I mean "logical" as in they represent a little better what we all see on the screen in the web console.

    Like I said, I don't have an AD infrastructure here where I can run your template, but I can try and convert the first one (which you said used to work) into SWQL.  It won't be immediate, but I'm hopeful that I (or someone else watching this thread) could lend a hand if they have the ability to test all parts.

  • It's definitely SQL - I can see that now.  I'll look to convert it to SolarWinds Query Language (SWQL) for you.  That way it should stay good for a much longer time.  Might take me a bit, but I'll give it some eyes.

  • I worked together a query that I think will work for you.

    You'll definitely need to change out a few of the elements.  When I mention "Unique IDs" I mean the things that are displayed in the Application Component editor.

    How to Find Unique IDs in Script Components

    Similarly, it inspired me to write up something I've been meaning to do for a while now.

  • Apologies, I've been meaning to reply to this for a few days.
    Thank you so much for the articles you wrote, they helped us in getting the widget back up and working! Your PIVOT example on the Multi-Statistic Data led us to the "MultipleStatisticData" table, where the information we were looking to retrieve exists on a single row!

    The solution that seems to be working for us starts with an Application Monitor running the PowerShell script as defined in my original post for each of the monitored accounts. The widget then uses the following SWQL code:

    SELECT
    
    CASE WHEN [Test].MultipleStatisticData.NumericData  <= 4 THEN CONCAT('<font color = red ><b>', [Test].MultipleStatisticData.NumericData ,'</b></font>')
     WHEN [Test].MultipleStatisticData.NumericData  <= 7 THEN CONCAT('<font color = #FFCC00><b>', [Test].MultipleStatisticData.NumericData ,'</b></font>') 
    
    ELSE CONCAT('<font color = green>', [Test].MultipleStatisticData.NumericData ,'</font>') END AS DaysLeft,
    
           [Test].MultipleStatisticData.StringData AS Date,
           [Component].Name,
    
    CASE WHEN [Component].MultipleStatisticData.StringData = 'True' THEN CONCAT('<font color = red ><b>', 'Locked','</b></font>') 
    WHEN [Component].MultipleStatisticData.StringData = 'False' THEN CONCAT('<font color = green>', 'Unlocked','</font>') 
    ELSE  CONCAT('<font color = #FFCC00><b>', 'Unknown','</b></font>') END AS LockOutStatus
    
    FROM Orion.APM.Component AS [Test]
    
    JOIN Orion.APM.Component AS [Component]
    
    ON [Test].ComponentID = [Component].ComponentID
    
    WHERE [Test].ApplicationID ='319' 
    AND [Test].MultipleStatisticData.Label = 'Password Expires (in days)' 
    AND [Component].MultipleStatisticData.Label = 'Lockout Status'
    
    ORDER BY DaysLeft

    This seems to be working as it generates the following table (account names edited out):

    Once again, thanks for your help. Between the PIVOT example and your article on Navigation Properties, it really pointed us in the direction we needed to go.

  • I'm so glad it worked for you.  If there's one thing I say all the time it's that the SolarWinds Platform's superpower is its flexibility.

    If you polish up the SAM Template and the report, you might consider posting them in the Application Monitor Templates and the Reports Content Exchanges so other people can benefit.

    THWACK is about sharing.