golbiga
Contributor III
Contributor III
Recently, a customer inquired about whether macOS can block AirPrint via a configuration profile. Unfortunately for the customer, the allowAirPrint restriction is iOS only (https://jamf.it/jpRgP). Because it can't be blocked, I decided to look into how to Jamf Protect could be used to report on print usage in macOS. 
 

Jamf Protect

Jamf Protect offers the following for generating alerts on a macOS endpoint:
  • Threat Prevention
  • Analytics
  • Unified Logs
  • Telemetry
  • Device Controls
Threat Prevention and Device Controls are out of scope for this use case, so lets take a look at Telemetry, Unified Logs, and Analytics. 
 

Jamf Protect Telemetry

Telemetry is a new feature in Jamf Protect that allows customers to send audit events generated on their macOS devices to their SIEM of choice. Telemetry currently uses the BSM implementation in macOS, so I can use the pre-existing binaries that I would normally use to analyze the logs locally. 
 
Running sudo praudit -sx /dev/auditpipe in Terminal does show print events, however none of this data would be useful for this exercise. 
<record version="11" event="AUE_ssauthorize" modifier="0" time="Tue Feb 14 22:14:04 2023" msec=" + 143 msec" >
<subject audit-uid="allen.golbig" uid="allen.golbig" gid="staff" ruid="allen.golbig" rgid="staff" pid="2578" sid="100013" tid="6357 0.0.0.0" />
<text>system.print.admin</text>
<text>client /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/printtool</text>
<text>creator /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/printtool</text>
<return errval="success" retval="0" />
<identity signer-type="1" signing-id="com.apple.authd" signing-id-truncated="no" team-id="" team-id-truncated="no" cdhash="0x1cb1be1d7de2da830093598ce3361797ce861fe3" />
</record>

Unified Logs

Unified Logs (UL) are the logging API in macOS that replaced the Apple System Log (ASL) back in macOS 10.12. In Jamf Protect, customers are able to add Unified Log predicates (default level only), which generate alerts when triggered. Much like Telemetry, Unified Log events must be sent to a customer’s SIEM. Because the macOS printing system is built off of the Common Unix Printing System (CUPS), I figured that’s where I should start.
 
log stream --predicate 'eventMessage contains "cupsd"' spits out a ton of information, but nothing that I can filter with Jamf Protect to generate an alert. My Jamf Protect plan log level is set to verbose (which you normally shouldn't leave on), but it gave me a clue where to look next.
 
[com.jamf.protect.daemon:Process] Checking: EXEC: (17100) cupsd 
 

Jamf Protect Analytics

Analytics are the behavioral detections in Jamf Protect. When triggered, these events are sent to both Jamf Protect and/or your SIEM. Jamf provides well over 150 built-in analytics, but we’re also able to create custom analytics to trigger alerts for events just like these. Before we go into the NSPredicate and NSExpression that I ended up using, lets go over how I took that Unified Log message and figured out what exactly to look for. 
 
There are many apps (free) which you can use to monitor Process events in macOS:
 
 
For this write-up I'm going to use ESFPlayground. After creating a blank text file (gotta conserve that toner), I’m going to launch ESFPlayground and keep an eye out as I print the blank doc. The first thing I noticed was that I generated seven, yes seven ES_EVENT_TYPE_NOTIFY_EXEC events. Looking closer, only one of them had a process_path I wanted, /usr/libexec/cups/backend/socket:
{
  "event" : "ES_EVENT_TYPE_NOTIFY_EXEC",
  "process" : {
    "session_id" : 70982,
    "ruid" : 0,
    "uid" : 0,
    "euid" : 0,
    "tty" : "None",
    "ppid" : 70982,
    "path" : "/usr/libexec/cups/backend/socket",
    "responsible_pid" : 70982,
    "username" : "root",
    "command" : " socket://BRN30055C300D7A.local.:9100/ 150 allen.golbig test 1 AP_ColorMatchingMode=AP_ApplicationColorMatching AP_D_InputSlot= nocollate ColorModel=Gray com.apple.print.DialogDismissedBy=Print com.apple.print.DocumentTicket.PMSpoolFormat=application/pdf com.apple.print.JobInfo.PMApplicationName=TextEdit com.apple.print.JobInfo.PMJobName=test com.apple.print.JobInfo.PMJobOwner=allen.golbig com.apple.print.PageToPaperMappingMediaName=Letter com.apple.print.PageToPaperMappingType..n.=1 com.apple.print.PDEsUsed=TextEdit com.apple.print.PrinterInfo.PMColorDeviceID..n.=27237 com.apple.print.PrintSettings.PMColorSpaceModel..n.=1 com.apple.print.PrintSettings.PMCopies..n.=1 com.apple.print.PrintSettings.PMCopyCollate..b. com.apple.print.PrintSettings.PMDestinationType..n.=1 com.apple.print.PrintSettings.PMDuplexing..n.=1 com.apple.print.PrintSettings.PMFirstPage..n.=1 com.apple.print.PrintSettings.PMLastPage..n.=2147483647 com.apple.print.PrintSettings.PMPageRange..a.0..n.=1 com.apple.print.PrintSettings.PMPageRange..a.1..n.=2147483647 com.apple.print.totalPages..n.=1 DestinationPrinterID=Brother_HL_2270DW_series media=Letter PaperInfoIsSuggested..b. pserrorhandler-requested=standard job-uuid=urn:uuid:0cb6a68a-5fbb-3a19-7522-64b9aef4b512 job-originating-host-name=localhost date-time-at-creation= date-time-at-processing= time-at-creation=1676433636 time-at-processing=1676433636 document-name-supplied=test job-impressions=1 com.apple.print.PrintSettings.PMTotalSidesImaged..n.=1 sides=one-sided Duplex=None com.apple.print.PrintSettings.PMTotalBeginPages..n.=1 PageSize=Letter",
    "team_id" : "",
    "pid" : 71070,
    "original_ppid" : 70982,
    "pgid" : 71070
  },
  "timestamp" : "2023-02-14 23:00:37"
}

I’ll need the signing info for both the process and parent_process now. Running codesign -dv /usr/libexec/cups/backend/socket, I get the following:

Executable=/usr/libexec/cups/backend/socket
Identifier=com.apple.socket
Format=Mach-O universal (x86_64 arm64e)
CodeDirectory v=20400 size=713 flags=0x0(none) hashes=17+2 location=embedded
Platform identifier=14
Signature size=4442
Signed Time=Feb 10, 2023 at 12:51:37 PM
Info.plist=not bound
TeamIdentifier=not set
Sealed Resources=none
Internal requirements count=1 size=64

Because this is a printing process, cupsd is the obvious parent_process, running pgrep cupsd outputs 70982, which matches the ppid in the ESFPlaygound output above. Lets grab the codesign info from /usr/sbin/cupsd.

Executable=/usr/sbin/cupsd
Identifier=com.apple.cupsd
Format=Mach-O universal (x86_64 arm64e)
CodeDirectory v=20400 size=3560 flags=0x0(none) hashes=106+2 location=embedded
Platform identifier=14
Signature size=4442
Signed Time=Dec 17, 2022 at 2:38:11 AM
Info.plist=not bound
TeamIdentifier=not set
Sealed Resources=none
Internal requirements count=1 size=64
Note: If you aren’t sure what the parent_process is for an event, you can use another great app, TrueTree (https://themittenmac.com/the-truetree-concept/). For this example when I run the TrueTree binary shortly after printing, I can search for that ppid
/System/Library/LaunchDaemons/org.cups.cupsd.plist
        /usr/sbin/cupsd   70982
With both identifiers I can build out a simple custom process event analytic:
$event.type == 1 AND
$event.process.signingInfo.appid == "com.apple.socket"
$event.process.responsible.signingInfo.appid == "com.apple.cupsd"

One last thing before checking alerts in Jamf Protect. In the ESFPlayground output, under process_command there was a very long string of text. This is the command line output that the socket binary passes when sending a print job. There will be a lot of useful data contained in that string and thats where Context Items come into play. Context Items are optional values in Analytics which help define additional conditions that an analytic can evaluate. 

Name: print job command
Type: String
Expression: event.process.commandLine

Not Everyone Uses a Brother Printer?

I shared this analytic with a few folks to test and it worked perfectly for them, as long as they had the same brand printer as mine. 
 
In macOS a printer can be added using the following protocols via System Settings:
  • Internet Printing Protocol (IPP)
  • Line Printer Daemon (LPD)
  • HP Jetdirect - Socket
  • AirPrint (combination of IPP and printer discovery)
  • USB
I’ll need to modify $event.process.signingInfo.appid to an array so the analytic can take into account other printing protocols. Running the following from /usr/libexec/cups/backend, I get the list of identifiers:
# Switch to root
sudo -s

# Run for loop to grab identifiers
for i in /usr/libexec/cups/backend/*; do codesign -dv $i 2>&1 | awk -F'=' '/^Identifier/ {print $2}'; done
com.apple.bluetooth
com.apple.dnssd
com.apple.ipp
com.apple.ipp
com.apple.ipp
com.apple.ipp
com.apple.dnssd
com.apple.lpd
com.apple.dnssd
com.apple.riousbprint
com.apple.smb
com.apple.snmp
com.apple.socket
com.apple.usb
The following identifiers are what I need to have in my custom analytic:
  • com.apple.socket
  • com.apple.ipp
  • com.apple.lpd
  • com.apple.usb
After adjusting the analytic to take into account the additional protocols, it should look like:
$event.type == 1 AND
$event.process.signingInfo.appid IN {'com.apple.socket', 'com.apple.ipp', 'com.apple.lpd', 'com.apple.usb'} AND
$event.process.responsible.signingInfo.appid == "com.apple.cupsd"
Here is what the final analytic should look like in Jamf Protect:
 
y6fI3gtg.png
 
Finally, let’s take a look at what an alert will look like in Jamf Protect:
 
ejQVSMSM.png
 

SIEM Example

Now that we have our analytic created and assigned to our Jamf Protect Plan, lets see what an alert would look like in a SIEM if configured in your action configuration. Below is an example query that I’m currently kicking around for querying the alert data in Splunk, along with a screenshot of the data sent from the alert shown above.
index="jamfprotect" input.host.hostname=* input.eventType="GPProcessEvent" input.match.facts{}.name="Printer_Activity"
| eval time=strftime(_time, "%m-%d-%Y %H:%M:%S")
| eval address = mvindex('args',0)
| eval user = mvindex('args',2)
| eval file = mvindex('args',3)
| rex field="input.match.context{}.value" "DestinationPrinterID\=(?<printer>[\w]+...)\s"
| rex field="input.match.context{}.value" "PMApplicationName\=(?<responsible_application>[\w]+...)\s"
| table time, user, file, printer, address, responsible_application

sadbAefM.png

GitHub Repo

To find the printer activity custom analytic and more, head over to the Jamf Protect Open Source Repository on GitHub. Within the repo are resources that can be adapted to any Jamf Protect tenant for the following areas, and more:
  • Custom Analytics
  • Unified Log Filters
  • Jamf Protect API Scripts
  • SOAR Playbooks
  • Third Party Integrations
  • Jamf Pro Extension Attributes

Happy printing!

 

7 Comments
matt_benyo
New Contributor
New Contributor

Love your process!

rastogisagar123
Contributor II

One of the best use case I have ever seen using Jamf Protect. Kudos to you Golbiga!!

ThijsX
Valued Contributor
Valued Contributor

Great write up buddy!

MatthewW
New Contributor
New Contributor

Fantastic work!

garybidwell
Contributor III

We've had this in use now for the last couple of weeks and been a game changer for our Inside Risk team in gaining visibility to what being printed from our macOS devices.
It's a big thank you from us in creating this.

rlegg
New Contributor
New Contributor

Great write up! Love seeing the thought process you went through.

ThomasBosboom
New Contributor

Thanks for the great write-up!