top of page
  • kevinkeathley3

Case Study: FPL for SentinelOne Threat Analysis

Updated: Oct 11, 2023

With the Fluency Program Language (FPL), Fluency has added the ability to assist analysts in handling tickets in an automated fashion. There are cases in which an analyst looks at a ticket, follows a runbook to verify all situations have been handled by the EDR, and closes the ticket. These situations force the analyst(s) to spend valuable time doing verification duty when they could be handling more important cases. FPL adds the ability to perform this duty on its own in certain cases. Here is one such situation.


Endpoint detection and response (EDR) systems are not perfect and cannot handle all variations of malware that they encounter. Many EDR users do not realize that an EDR sometimes fails to respond properly. This FPL example is an automated review of SentinelOne alerts and its responses.


Fluency is a SIEM that clusters alerts by entities (UEBA). It also analyzes in a streaming mode, so the entity clusters can be updated at any moment. As you read, notice that SentinelOne alerts are shown already grouped in cases, and additional machine learning is tagged for anomalies.


SentinelOne Threats

Case 1: The Trivial Case

A SentinelOne alert normally occurs with a combination of four alerts (SentinelOne Threat, SentinelOne Process Killed, SentinelOne Quarantine OK, and SentinelOne Management) as seen here:



For each SentinelOne Threat event, there should be follow-on events indicating that the threat process was killed and the corresponding file was quarantined. An analyst typically needs to review the ticket to ensure that this is the case. The threat will give the analyst one or more hashes along with one or more filenames (in a one-to-many relationship). Once the analyst verifies that all hashes and files have been killed and quarantined, and that there are no more alerts in the ticket (possibly unrelated to SentinelOne), only then can the analyst close the ticket and move on to other business.

With FPL, Fluency can perform this task without analyst intervention!



Now questions remain as to what about cases that are not so simple. What else can happen?

  • SentinelOne could fail to quarantine the threat

  • There may be a Dangling Quarantine (explained later)

  • SentinelOne could fail to kill the threat process that is running

  • There may be no hash to quarantine (ex: memory-only threats)


Either the analyst or Fluency must be able to handle these cases in an accurate manner before closing any tickets. Otherwise, a closed ticket may still contain an active threat, or an earlier ticket may remain open when it should be closed.

Let us look at the cases.


Case 2: Failed Quarantine

It sometimes occurs that there is a failed quarantined. This should be additional logic of the trivial case.


When the ticket contains an alert that a quarantine failed, it must not be closed until that issue is handled. In most cases, the analyst needs to further investigate and/or hand this ticket off to those responsible for incident response. The ticket itself will need to remain open until the issue is cleared. In this case, the source of the threat was not quarantined and may still pose a threat.


Case 3: Failed to Kill Process

Some threats may be more resilient than others, preventing the EDR from killing its running process. This may be identified either by the lack of a SentinelOne Process Killed alert or the presence of an alert indicating the EDR failed to kill the process.


In this case, a threat was found, and the file was quarantined, but for some reason the EDR did not kill the invading process (or for that matter report that it failed to kill it – perhaps because it could find one). An analyst or incident responder will need to investigate the running processes on this system to ensure the threat is not still active before closing the ticket.


Case 4: No Hash to Quarantine

Sometimes the EDR finds a threat, either as a running process, a malicious file on the system, or perhaps even both. However, in this case it only identified that there was a threat (perhaps a running process), but could not identify a file associated with that threat.



In many cases this means that a memory-only threat has been found. It may have been injected into another process or inject remotely by some other means, leaving nothing on the filesystem to quarantine. In rare cases, the EDR may have just not found the original file associated with the threat. This could also indicate that it was related to an interactive session, also a case where there was no file.


The analyst or incident responder will need to scan the system to ensure that a malicious file was not left by not being identified by the EDR.

Case 5: Dangling Quarantine (a special case)

The Dangling Quarantine case is a more complex example of where the EDR identified a threat, but not all the information is in the same ticket.


As can be seen, in an earlier ticket, a threat was identified by the EDR, the threat process was killed, but the threat was not quarantined (seen as the Sentine One Quarantine Failed alert). Later on, perhaps the next day, another ticket was created with an alert specifying that a successful quarantine occurred. However, the new ticket does not indicate that there was a threat. Tickets are typically created each day there are events for that day meaning that if a set of events happened before and after midnight (or a longer period), then the events will end up on separate tickets.


An analyst must manually conduct research by looking at prior tickets (one or more days old) and comparing hashes to identify the source of the threat. Once the original ticket has been found where the threat was identified and the analyst verifies this was the threat that was closed, then both tickets can be closed together (assuming neither ticket has any other pending issues). This can take a considerable amount of time depending on the number of tickets the analyst has to search and the number of times that hash appears.


The Fluency Programming Language Solution

As mentioned, the release of FPL by Fluency means that these cases as well as many others can be easily automated. Fluency itself can perform the various logic checks that previously required an analyst and close the tickets itself. This capability is only possible because FPL was built as an integrated language that melds the underlying query system, event streams, patented LavaDB, and programming language into one fluid system. All these systems are native to the programming language, unlike systems where a language being used must import and/or export data from unsimilar parts. In FPL, a Table and Row from the event stream are just as native to the language as an integer or string.


Fluency has done this with the SentinelOne Threat Anaylzer, an FPL program that performs to role of an analyst in this particular cases. Here are some pseudo-code (English language interpretation of the programming logic) of the use-cases, built from an actual runboook:


Case 1: The Trivial Case

  1. Trigger of an Event Summary with a “SentinelOne Threat”

  2. If there are more than four alerts, make a note of which additional alerts occurred.

  3. For each hash:

    1. Validate a process kill, if not make a note.

    2. Validate a file quarantine, if not make a note.

  4. Close the ticket when:

    1. Validate that there are one four (4) alerts.

    2. All hashes are killed and quarantined.

    3. Make a note that it was closed by this program.

  5. Else,

    1. Make a note that it was not closed by this program.


Case 2: Failed Quarantine

Additional Steps to the Trivial Case:

  1. There is a “SentinelOne Quarantined Failed” alert (or that there was a missing match between threat and quarantined).

  2. Ensure that the alert is critical.

  3. Add an additional note that is a list of failed quarantined file hashes.


Case 3: Failed to Kill Process

Additional Steps to the Trivial Case:

  1. Identify hashes associated to the threat that EDR was unable to kill

  2. Add an additional note that is a list of the hashes that are unresolved


Case 4: No Hash to Quarantine

Additional Steps to the Trivial Case:

  1. Identify hashes and filenames (if any) associated with the threats that were not quarantined

  2. Add an additional note that is a list of the hashes that are remaining (or not found)


Case 5: Dangling Quarantine (a special case)

  1. Trigger on an Event Summary that has a Quarantine, but not a Threat (or has a quarantined hash where no Threat exists in this ticket for that hash.

  2. Search the previous week for this SentinelOne agent and file hash.

  3. Look at the note section, then:

    1. remove the “not quarantined“ for this hash.

    2. If a there are no more “not quarantined“ and “not killed” notes/events then change, this ticket to resolved.

  4. If there are only SentinelOne alerts on the old ticket, and if it passes the other use cases, then close it and make a note.

  5. Close the original Dangling Quarantined OK summary and make a note.


End Case: Final Supplementary Report

  • Provide a final report containing:

  • Failed Quarantines

    • Which systems still have bad files

    • What files are remaining

  • Failed to Kill Process

    • Which systems were affected

    • What files/hashes were not killed (if available)


FPL Code Samples


Pull Ticket from Behavior Summary List

/***********************************************************************************
 * Returns the Behavior Summary identified by id
 * If not objects are found, returns an empty list                                 
 * @param {string} id the id of the behavior summary to return
 * @return {Table} a table containing the results of the behavior summary query
 **********************************************************************************/
function getBehaviorSummaryById(id) {
    let query = `id:"{{.id}}"`
    let behaviorSummaryList = fluencySummarySearch(template(query, { id: id }), "-90d@m", "@m", (obj) => {
        let { id, behaviorRules, riskScore, key, dayIndex, status, risks, summaryList } = obj
        if (!obj.summaryList.Some((_, summary) => summary.behaviorRule == "SentinelOneThreat")) {
            return null
        }
        return { id, behaviorRules, riskScore, key, dayIndex, status, risks, summaryList }
    })
len(behaviorSummaryList))

    return behaviorSummaryList
}

Gather List of SentinelOne Threats

/********************************************************
         * Retrieve List Of Threats                             
         *******************************************************/
        let threatList = []
        threatList = concat(threatList, behaviorSummary.summaryList.Filter((_, summary) => summary.behaviorRule == "SentinelOneThreat"))

        let fileHashes = []
        let filePaths = [] // This was added and required for reporting only
        if (Validate(threatList)) {
            threatList.Each((_, threat) => {
                if (Validate(threat)) {
                    if (Validate(threat)) {
                        let hashes = []
                        fileHashes = concat(hashes, threat.attributeSummaries.Find((_, att) => att.aliase == "FileHash")?.values)
                        filePaths = concat( filePaths, threat.attributeSummaries.Find((_, att) => att.aliase == "FilePath")?.values)
                    }
                }
            })
        }

Identify Quarantined Threats

/****************************************************************************************************
            * @requirement {Failed Quarantine Case} Validate a file quarantine, if not make a note.             
            * @requirement {Failed Quarantine Case} CASE 1: //There is a “SentinelOne Quarantined Failed” alert 
            * @requirement {Failed Quarantine Case} Ensure that the alert is critical.
            ****************************************************************************************************/
            if (quarantineFailedFileHashes.Some((_, quarantined) => quarantined == hash)) {
                AddNoteToSummary("SentinelOneThreatAnalyzer", behaviorSummary.id, "Attempt to quarantine the following hash has failed: " + hash)
                notesList = append(notesList, { ID: behaviorSummary.id, Note: "Attempt to quarantine the following has failed: " + hash })
                if (behaviorSummary.riskScore < 8000) {
                    SetBehaviorSummaryScoreAdjustment("SentinelOneThreatAnalyzer", behaviorSummary.id, 8000 )
                }
                close = false
                unquarantinedHashes = append(unquarantinedHashes, { ID: behaviorSummary.id, FileHash: hash })
                if (Validate(openTickets[behaviorSummary.id])) {
                    let newList = append(openTickets[behaviorSummary.id], ", Quarantine Failure")
                    openTickets =delete(openTickets, behaviorSummary.id)
                    openTickets[behaviorSummary.id] = newList
                } else {
                    openTickets[behaviorSummary.id] = "Quarantine Failure"
                }
            }

Final Close or Leave Open Process

if (close) {
            /*******************************************************************************************************************
            * @requirement {Simple Close} Close the ticket when:                                                               
            * @requirement {Simple Close}  Validate that there are one four (4) alerts.                                        
            * @requirement {Simple Close}  All hashes are killed and quarantined.                                              
            * @requirement {Simple Close}  Make a note that it was closed by this program.                                     
            *******************************************************************************************************************/

            if (behaviorSummary.status == "new" || behaviorSummary.status == "acknowledged" || behaviorSummary.status == "resolved") {
                AddNoteToSummary("SentinelOneThreatAnalyzer", behaviorSummary.id, "Closed automatically by Fluency.")
                notesList = append(notesList, { ID: behaviorSummary.id, Note: "Closed automatically by Fluency." })
                AddNoteToSummary("SentinelOneThreatAnalyzer", behaviorSummary.id, "*** FINISHED ANALYZING ***")
                try {
                    CloseBehaviorSummary("SentinelOneThreatAnalyzer", behaviorSummary.id)
                    closedTickets = append(closedTickets, behaviorSummary.id)
                } catch (error) {
                    let msg = "Unable to close summary due to " + error.name + ": " + error.message
                    failureToCloseList = append(failureToCloseList, { ID: behaviorSummary.id, Reason: msg })
                }
            }else{
                alreadyClosedTickets = append(alreadyClosedTickets, behaviorSummary.id)
            }

        } else {
            /***********************************************************************************
            * @requirement {Simple Close}  Else,                                               
            * @requirement {Simple Close}  Make a note that it was not closed by this program. 
            ***********************************************************************************/
            AddNoteToSummary("SentinelOneThreatAnalyzer", behaviorSummary.id, "NOT closed automatically by Fluency.")
            notesList = append(notesList, { ID: behaviorSummary.id, Note: "Not closed automatically by Fluency." })
            AddNoteToSummary("SentinelOneThreatAnalyzer", behaviorSummary.id, "*** FINISHED ANALYZING ***")
            //openTickets = append(openTickets, behaviorSummary.id)
        }



90 views0 comments

Recent Posts

See All
bottom of page