Tailored Event Monitoring on macOS
  • 09 Jan 2024
  • 23 Minutes to read
  • Dark
  • PDF

Tailored Event Monitoring on macOS

  • Dark
  • PDF

Article summary


If you've navigated here, you're likely interested in learning how to write your own custom analytics for custom macOS event monitoring. This resource is designed to guide you through the process!

You might be asking yourself, "What exactly are custom analytics, and why would I need them?"

To clarify right at the start, for protection of your macOS endpoints with Jamf Protect against threats or for detecting suspicious user or system activity, crafting custom analytics isn’t a necessity. Jamf-managed analytics and threat prevention capabilities offer extensive coverage, monitoring real-time, event-driven activities on macOS systems.

However, there are scenarios where security analysts seek to track activities specifically targeted at their organization. In such cases, custom analytics, functioning as predicate-based filters, can be particularly beneficial. But, before delving into custom analytics, it's important to understand the situations in which they are truly needed.

Eager for examples and want to skip straight to the practical side? Check out the examples!

Deciding when to implement custom Analytics

The more you familiarize yourself with Jamf Protect, the more you'll realize its ability to collect valuable data from endpoints, crucial for security and IT teams. However, determining which specific area of Jamf Protect can provide the necessary data for your particular use case or scenario may require some guidance or research.

If the goal is to implement a pre-defined solution that offers both broad and deep macOS event monitoring, telemetry would be more appropriate.

Before we delve into how to start writing custom analytics, let's first review the types of events and data streams that Jamf Protect can set up. Understanding these will better inform your decision on whether a custom analytic is necessary, either to collect specific information from an endpoint or to detect activities unique to your organization or endpoints that are not covered by Jamf-managed analytics.

Given that you have likely already explored Jamf Protect, you've probably noticed multiple areas where custom items can be created. Let's take a closer look at these areas before proceeding.

  • Analytics
    • Analytics are predicate based rules that detects suspicious user behaviour and malicious system activity on macOS computers, Jamf Threat Labs creates and maintains Jamf-managed Analytics monitoring and alerting up suspicious activity or behaviour.
    • Custom Analytics
      • Custom Analytics provides security teams a framework to create custom predicate based rules to start monitoring for specific activity happening on managed endpoints. Depending on the chosen sensor type activity can be monitored and reported on. We’ll go deeper in to this further down in the write-up.
  • Telemetry
  • Unified Logging
    • The Unified Logging system on macOS provides a central location to store log and event data on the Mac, with Jamf Protect, you can use predicate-based filters to collect relevant log entries from computers and send them to a security information and event management (SIEM) solution or a third party storage solution (e.g., AWS).
  • Threat Prevention
    • Threat Prevention provides prevention capabilities to detect and block threats up on execution, meaning that Jamf Protect monitors each process execution and allow or deny the operation and execution of the process based on the fact if there is an match in Jamf Protect’s threat database.
    • Custom Prevention Lists
      • Custom prevention lists allow you to specify additional processes to block using Jamf Protect based on file hashes and Apple-specific signing information.
  • Network Threat Prevention
    • Network Threat Prevention offers web threat prevention on-device to keep your users and system safe from cyber threats. This protection includes vulnerability assessments, network-based security protections and content-filtering.

Now that we have reviewed this, it’s clear that Jamf Protect provides an extremely powerful framework to gather an extensive list of curated information from macOS endpoints out-of-the-box or provides the tools to create custom rules to gather additional information from macOS endpoints.

More and more security teams are keen to get additional information from an endpoint or start monitoring activity that is specific to their organization or user group. Without knowing the capabilities of Jamf Protect, administrators usually start creating custom analytics to detect certain activity or behavior, while this is not always necessary as the data that we might need for detection is already being collected by telemetry.

As you know, endpoint protection does not come for free from a system resource perspective, meaning we need to be mindful when creating custom analytics or Unified Log filters and really evaluate if this data is not already collected by default by telemetry, thus avoiding the duplication of collecting data. Both telemetry and Unified Logging require integrating with a SIEM or third-party storage solution, so if that’s not yet in place, or on the roadmap of the organization, then yes, we might already have more reasons to rely on leveraging custom analytics.

Let’s take a look at some use cases to provide some context:

  • As a security analyst, I want to monitor process executions that are not signed with a valid Developer Certificate or unsigned at all.
    • While this could be tracked with a custom analytic and generate an alert, Telemetry Level 1 already reports on process executions and contains the related signing information as well. We could use this data in a SIEM, saving us from creating a custom analytic and preserving system performance.
  • As a security analyst, I want to monitor tamper events related to the Jamf Pro management framework.
    • This would be a good use case for writing a custom analytic. However, Jamf Protect already provides detections for this activity using a set of Jamf-managed analytics.
  • As a security analyst, I want to monitor a particular file being created, renamed, or modified on a specific file path by anyone except a particular user UID.
    • This is a perfect use case for creating a custom analytic where we can monitor file system events and use a set of criteria to detect this activity and generate an alert.
  • As a security analyst, I want to keep track of user and group creation or changes across all my endpoints.
    • Telemetry by default keeps track of this activity and generates events that inform you as these changes happen. AUE_create_group or AUE_create_user are relevant examples to review.
  • As a security analyst, I want to keep track of files being written to a removable storage device by a specific process.
    • There are default Jamf-managed analytics that provide informational alerts on USBWrite events, or telemetry information about a drive being mounted. However, custom analytics could be used here to detect specific files or file types being written to an external volume by specific processes, and could generate a low or medium severity alert.

The examples provided above are basic, yet they offer valuable context to consider before you continue on building a custom analytic. Below, you’ll find a simple diagram that can serve as a useful reference.

Diagram to help decide if you need to build a custom Analytic

Now that we have this covered, you can decide if we need to create a custom analytic to capture specific activity or if we can rely on the above-mentioned features of Jamf Protect.

If it’s the former, let’s continue to the second section. If it’s the latter, then this is a good future reference to revisit!

Fundamentals of creating (custom) Analytics

As you have already read in the introduction, analytics are predicate-based filters that provide capabilities for monitoring specific user or system activity on macOS endpoints. Once the conditions are met for an event, an alert is triggered, providing the relevant context to the security administrator in the Jamf Protect macOS Security portal.

Jamf-managed analytics are provided and kept up to date by Jamf Threat Labs to offer detection and protection against novel security events, including thousands of previously unknown and undiscovered threats.

Just as Jamf Threat Labs employs analytics for detection purposes, security analysts or administrators can similarly leverage custom analytics to create their own detections.

Let's review the basic requirements of a custom analytic in order to exist in Jamf Protect before we go deeper into the specific sensor types, the predicate, query language, and the available documentation.

Each custom Analytic does require to to have at minimum

  • Analytic Description
    • Name
      • The name provided by the security administrator will be the name of the Analytic, but also the name of the alerts that could be generated as well. TIP: be mindful about a name format that makes sense to your security team, try to keep it consistent and specific.
    • Description
      • It’s optional to provide a description, but it’s strongly recommended. As you are building a custom analytic, it’s not always easy for anyone to read the predicate filter and understand what it’s detecting. Provide a description of the event once detected, as this will be the summary in the alert that gets created.
    • Category
      • Optionally select the categories to associate with the custom analytic or define your own categories in the dropdown menu. This data is exposed in generated alerts and forwarded data to a SIEM or 3rd party storage solution as well.
    • Level
      • For now, we’ll leave the level set to 0, but this can be used to build chained analytics where potentially a level 1 or level 2 analytic can only be triggered if a lower level has been triggered, and they have been linked to each other using Tags.
  • Severity
    • Select the severity that you would like to associate with the custom Analytic.
      • If informational is chosen as severity, ensure that in the action configuration for sending alert data to Jamf Protect, the minimum severity is set to Information as well.
      • If alerts are being forwarded to a SIEM or 3rd party storage solution, then the severity is represented in integers where 0=informational, 1=low, 2=medium, 3=high.

We have now covered the basics that each custom analytic requires, but the analytic doesn't mean anything until we have defined what type of activity it's going to monitor and have provided a valid predicate filter.

Exploring the range of Sensor Types in Analytics

As an important step during the creation of a custom analytic, we need to select the type of behaviour or activity we initially want to monitor. The chosen sensor type defines what type of detection we are able to create.

Below, we will explain the various sensor types that are available in Jamf Protect and what they are capable of providing.

  • Analytic Filter
    • Sensor Type
      • File System Event
        • This event provides capabilities in to detecting the creation, renaming, modification or deletion of files or directories.
      • Download Event
        • This event provides capabilities in to detecting files being downloaded from the internet on to the file system using the Safari Sandbox broker.
      • Process Event
        • This event provides capabilities in to detecting a process being launched or terminated and containing the relevant subtypes (eg. exec, fork, posix_spawn, execve).
      • Screenshot Event
        • This event provides capabilities in to detecting a process using the ScreencaptureKit framework on macOS and is creating a file on to the file system.
      • Keylog Register Event
        • This event provides capabilities into monitoring for new "event tap" registrations via the Core Graphics framework on macOS. Core Graphic event taps are often used by certain types of key logging and accessibility software.
      • USB Event
        • This event provides capabilities into monitoring the insertion or removal of USB devices from an endpoint.
      • Gatekeeper Event
        • This event provides capabilities into monitoring actions and logs from Gatekeeper, Apple's built-in feature for enforcing code signing and verifying downloaded apps before opening them.

That's an extensive list of sensor types, and for each analytic, we can only pick one. We’ll leave this as it is for now and jump into explaining the predicate language.

Understanding the critical role of predicates in Analytics

A custom analytic is not worth much without a valid predicate, which is essentially a rule containing a set of criteria and conditions that need to be met in order to trigger an alert.

The ability to build custom predicates is really valuable but comes with great responsibility. Any minor mistake or overly broad set of criteria can either trigger tons of alerts or heavily impact system performance on its endpoints.

Jamf Protect uses Apple’s own NSPredicate and NSExpression query language for analytics, similarly to how we can use filters to search or stream logs using the Unified Logging subsystem.

Fortunately, Jamf Protect provides a built-in query builder and documentation that help you in building predicates. This can be especially useful when you are new to building custom analytics.

Before we delve into building a custom analytic using an example use-case, here are some things to remember while you plan to build them yourself in the future:


  • Aim to be as specific as possible with the conditions in your predicate to ensure targeted and efficient results
  • Prioritize the most restrictive or significant condition at the top of your predicate. This approach minimizes unnecessary evaluations. For instance:
    • If using MATCHES with a regular expression, place them towards the end of your predicate statement. This allows the predicate to filter objects first before undertaking more intensive look-ups and calculations.
  • When evaluating data that is an array, consider using the IN operator instead of multiple AND or OR conditions. This method is more efficient as it avoids recalculating and re-evaluating the same data set multiple times. An example of this would be:
    • $event.type == 1 AND (($event.process.pgprocess.signingInfo.appid IN {"com.jamf.management.daemon", "com.jamfsoftware.jamf"}) OR ($event.process.responsible.signingInfo.appid IN {"com.jamf.management.daemon", "com.jamfsoftware.jamf"}))


  • Avoid using overly broad conditions in order to preserve system performance and prevent generating too many alerts. For example:
    • An predicate for process events with only having one condition like $event.type == 1 as this would create an alert for each process being launched
    • Similar for file system events only having one condition like $event.type == 4 as this would create an alert for each file being modified
    • Avoid overly broad file paths if possible as example
      • $event.process.path BEGINSWITH "/"

Useful resources related to NSPredicate and NSExpression

Example custom Analytics

For practice we are going to walk through the process of building an four custom analytics using all of the above we have learned.

Each of them have their own purpose and we'll go through using various sensor types and predicate filtering

Example 1 - Keychain dumped

Let’s start with the following sample use case

As a security analyst i got tasked to detect the usage of the built-in security binary combined with using the arguments --dump-keychain

While the usage of the security binary with the --dump-keychain arguments is not malicious by it’s self, it’s not common that end-users on purpose are dumping contents of their keychain and therefore this could be an anomalie across an fleet of endpoints.

In order to get a good direction in what we might be looking at is mimicking this behaviour on a test endpoint while running a event monitor tool like Red Canary Mac Monitor

Having Red Canary Mac Monitor installed and running we are going to open up a Terminal and interact with the security binary to dump the keychain where we will run the following command security dump-keychain. We can now search for the security binary or review the process execution event metadata and we’ll quickly notice that there’s really useful information stored within it.

Let’s use the process path and command line outputs for this example.


As a next step we need create a custom analytic in Jamf Protect and use the provided Analytic Description below to get started

Name: keychain_dumped
Description: This detection functions by monitoring for process creation involving binaries carrying the signing information of 'com.apple.security' and using the arguments "dump-keychain
Categories: Visibility, CredentialAccess
Severity: Low

For this use case we want to start monitoring process events and therefore we need to select as sensor type Process Event. To start building the predicate let’s use the Filter Builder Query View as this will help you getting familiar with the data structure of Jamf Protect.

Each time there’s interaction with the security binary a process is being launched so we need to start our predicate by monitoring for processes being launched using $event.type == 1. Using the built-in documentation you will notice that we can choose between the numeric values 0=none, 1=create and 2=exit for $event.type related to the sensor type Process Event.

Then secondly we obviously only want to limit this detection to the execution of the security binary and we have various ways of pointing to it where we could use process name, the process path or the appid of the binary.

In this case we are going ahead with the appid of the binary, the reason why we are using the appid is the fact that this won’t change even if the path of the binary or the name of the running process has been changed.

To collect the appid we could run the following command in Terminal codesign -dv /usr/bin/security and from the given output collect the identifier which should be in this case com.apple.security and now we can go ahead and add an second condition in the predicate which should be as following $event.process.signingInfo.appid == "``com.apple.security``".

    Format=Mach-O universal (x86_64 arm64e)
    CodeDirectory v=20400 size=2667 flags=0x0(none) hashes=73+7 location=embedded
    Platform identifier=15
    Signature size=4442
    Signed Time=11 Oct 2023 at 08:28:27
    Info.plist=not bound
    TeamIdentifier=not set
    Sealed Resources=none
    Internal requirements count=1 size=68

If we would save the analytic as is it would generate an alert each time the security binary has been launched so we want to make it even more specific, we know what we are looking for which is the specific argument dump-keychain that we got from Mac Monitor which in this case is a single string value so we add another condition with the AND operator as following $event.process.commandLine CONTAINS "``dump-keychain

Now once we’ll save this analytic and assign it to an analytic set each time the security binary is launched with the dump-keychain arguments being passed on it will trigger an low severity based alert!

$event.type == 1 AND 
$event.process.signingInfo.appid == "com.apple.security" AND 
$event.process.commandLine CONTAINS "dump-keychain"

security dump-keychain analytic example in Query Builder View

We can tweak this predicate to make it even more precise if we want to, as example the analytic as we have been building it would capture if any process invokes the security binary and the related arguments or an end-user interacting with the binary using an interactive shell in an Terminal session.

Let’s consider in this use case we care less about the security binary and arguments being used in a Terminal but only want to generate an alert if it’s invoked by other processes we could add the following as the second condition to the predicate $event.process.tty == nil, the tty value will indicate no Terminal session is related to this running process.

The result would be the following predicate

$event.type == 1 AND
$event.process.tty == nil AND
$event.process.signingInfo.appid == "com.apple.security" AND 
$event.process.commandLine CONTAINS "dump-keychain"

TIP: If you wish to test this analytic locally in the Terminal, you should omit the $event.process.tty == nil AND condition.

Example 2 - TouchID enabled for sudo

Let’s review another use case where we are going to focus on particular file system events.

As a security analyst i would like to receive an informational alert once on an endpoint TouchID has been enabled for sudo.

Again for this use case we are not going to build this analytic in order to detect malicious activity but we would like to be informed in case TouchID has been added as additional method for using sudo.

Create a custom Analytic in Jamf Protect with the following generic information.

Name: touchid_sudolocal_enabled
Description: This detection functions by monitoring for file system modification events specifically related to etc/pam.d/sudo_local and TouchID being enabled for sudo.
Categories: Visibility
Severity: Informational

As a sensor type we need to select File System Event and there are various ways to detect certain file system activity.

Since macOS Sonoma an template file is provided with the file path /etc/pam.d/sudo_local.template which can be copied over to /etc/pam.d/sudo_local but we don’t want to monitor for just this file being added as by default it’s not enabled in the template.

So we’ll start with a strong condition in our filter which is $event.path == "/etc/pam.d/sudo_local" and secondly we want to add as a condition the SHA256 hex value of the file as that indicates the sudo_local is configured in a way it’s enabled and Apple expects it’s to be.

Once the template file is modified we can run a shasum -a 256 on the file and we should get back this particular hash value d0e654850da470b1fa823f02c4209bf6e01570c32b30b230064902947026bde4 which allows us to add an second strong condition to our filter $event.file.sha256hex == "d0e654850da470b1fa823f02c4209bf6e01570c32b30b230064902947026bde4".

As you may have seen in the documentation there are file create, rename, modified and delete events so we want to limit this as well only to create and modified events so we need to add
$event.type IN {0, 4} and if we would save it like this this would do the trick but it will trigger an alert each time the file has been modified even the data has not been changed.

We can optionally work with snapshots in Analytics which can take a snapshot of the related files before those are being modified and we can then use that as an additional condition to only trigger the detection if the raw data of the file is not the same as it was before being modified.

To do this for this example add the path /etc/pam.d/sudo_local to snapshot files.

Then by adding the following condition we are making sure the data of the current file is not similar to the data of the previous file once it’s a file modified event, this potentially could reduce any false positives.

$event.file.data != $event.file.snapshotData.data

The complete predicate woud look like the code block below in the filter text view.

$event.path == "/etc/pam.d/sudo_local" AND
$event.file.sha256hex == "d0e654850da470b1fa823f02c4209bf6e01570c32b30b230064902947026bde4" AND
$event.type  IN {0, 4} AND
$event.file.data != $event.file.snapshotData.data


Example 3 - sfltool resetbtm activity

Let’s do one more process event example

As a security analyst i want to be informed whenever the sfltool is launched and especially launched using the arguments resetbtm, this could be used to reset background task management and persistent login items and prompt the user to authorize new background task items, however this is not by default suspicious user behaviour as we do provide options through Jamf Self Service to have this reset and should not be reset in any other way.

Similar to the first example we are going to fill in the basics.

Name: sfltool_resetbtm
Description: Reports on the usage of the sfltool utility to reset background task management using the resetbtm arguments, this detection only triggers if the responsible parent process is not Jamf.
Categories: Visibility
Severity: Informational

Select as sensor type the process event and let’s start building our predicate filter either using the Filter Builder Query View or Filter Text View.

Using the similar methods explained in example 1 we can detonate this behaviour on an endpoint while we are having a tool running such as Red Canary Mac Monitor, and while doing so we can again find out that we need com.apple.sfltool as appid and as this is very specific to this custom analytic we are going to use this as first condition.

$event.process.signingInfo.appid == "com.apple.sfltool"

Now obviously we need to specify what type of process events we want to monitor and in this case it’s $event.type == 1 which indicates all types of process launch events.

As indicated earlier on, in this example use case workflows are provided through Jamf Self Service where an end-user can initiate a reset of background task management to potentially troubleshoot any issues or help cleaning up persistently installed software, our task here is to only trigger alerts if sfltool resetbtm is launched where the responsible process or parent process is not related to Jamf.

To do so we can add a grouped condition where we start with specifying the arguments do contain resetbtm and the parent process signing information is not equal to Jamf’s TeamID, yes this would make an exclusion anything that’s signed by Jamf’s TeamID but that’s okay for this example.

($event.process.commandLine CONTAINS "resetbtm" AND $event.process.parent.signingInfo.teamid != "483DWKW443")

Of course if preferred it can be narrowed down to appid or process path or name.

The complete predicate filter would look like this example in this code block.

$event.process.signingInfo.appid == "com.apple.sfltool" AND
$event.type  == 1 AND
($event.process.commandLine CONTAINS "resetbtm" AND  $event.process.parent.signingInfo.teamid != "483DWKW443")


Example 4 - tmutil activity

macOS employs APFS (Apple File System) and offers the capability to automatically create hourly system snapshots when Time Machine is enabled. This feature is particularly useful in case a system failure occurs during an upgrade, allowing for easy restoration of the operating system. Time Machine enables users to create backups either locally or on an external volume. tmutil, a command-line utility, provides various options to manage these backups, including creating, restoring, and deleting snapshots or backups.

Here we encounter another practical use case:

As a security analyst, I want to monitor the deletion of either local snapshots or backups on an external volume. Such behavior could suggest that an adversary has performed this action prior to launching a ransomware attack, aiming to prevent the victim from restoring their files.

The next step involves creating a custom analytic in Jamf Protect. We can use the provided Analytic Description below as a starting point.

Name: tmutil_activity
Description: This detection reports instances when tmutil is utilized outside of an interactive command line for specific purposes, either to delete all local snapshots using the deletelocalsnapshots argument with / as the mount point, or when the delete argument is used to remove a Time Machine backup from an external volume. Such actions could potentially indicate adversarial behavior, as an attacker might perform these operations to impede file restoration by the victim in the event of a ransomware attack.
Categories: Visibility
Severity: Medium

Similar to the first task, we can utilize a variety of tools to mimic the behavior and discover the AppIDs needed to create this predicate filter. Since we specifically want to focus on the tmutil command line utility, rather than the graphical user interface of Time Machine, we will concentrate on monitoring com.apple.timemachine.tmutil.

For the first condition, we’ll use the signing information and its related AppID as follows: $event.process.signingInfo.appid == "com.apple.timemachine.tmutil". As a second condition, we specify $event.type == 1 because we only want to monitor process launch events.

Next, we add a condition to monitor the usage of tmutil when the process execution is not occurring in an interactive terminal window and no tty is assigned to the process, with $event.process.tty == nil.

The process becomes slightly more complex as we proceed to use nested conditions to specifically monitor activities where the arguments deletelocalsnapshots and / or delete are being used as process arguments.

To create grouped conditions, we use parentheses to enclose them. If necessary, we can even incorporate multiple groups within a single group.

To achieve this, we can employ the following grouped conditions:

(($event.process.args CONTAINS[c] "deletelocalsnapshots" AND $event.process.args CONTAINS "/") OR ($event.process.args CONTAINS[c] "delete"))

With the complete predicate now formulated as shown below, we can proceed to save the analytic. Once saved, it can be attached to an analytic set, enabling us to start monitoring this specific activity.

$event.process.signingInfo.appid == "com.apple.timemachine.tmutil" AND
$event.type == 1 AND
$event.process.tty == nil AND
(($event.process.commandLine CONTAINS[c] "deletelocalsnapshots" AND $event.process.commandLine CONTAINS "/") OR ($event.process.commandLine CONTAINS[c] "delete"))

TIP: If you wish to test this analytic locally in the Terminal, you should omit the $event.process.tty == nil AND condition.


Looking for more examples?

For more examples, check out Jamf's Open Source Repository

Changing your password will log you out immediately. Use the new password to log back in.
First name must have atleast 2 characters. Numbers and special characters are not allowed.
Last name must have atleast 1 characters. Numbers and special characters are not allowed.
Enter a valid email
Enter a valid password
Your profile has been successfully updated.