Querying Spotlight APIs With JXA

Cedric Owens
5 min readFeb 19, 2022

TL;DR This blog post takes a brief look at how to use JXA (native JavaScript for Automation on macOS) to query Spotlight APIs. In particular, this post will be looking at how to run mdfind searches in JXA without invoking the on-disk mdfind binary.

Why?

  1. I enjoy writing scripts/utilities for macOS and often use Swift and JXA. When already in a coding context, it just doesn’t feel right to me personally going from code to call an on-disk binary and then going back to code when I can just make the API calls directly from code. I have found JXA to be a bit of a challenge at times when it comes to learning how to make certain API calls since there is limited documentation available publicly. However, since JXA will remain on macOS (while other scripting runtimes such as python, perl, and ruby are being removed from base macOS installs), I enjoy putting the time in to learn JXA a bit more.
  2. As a red teamer, moving away from the command line to API calls is always preferred, especially as the state of macOS detections continues to improve over time.

Let’s Dig In

Towards the end of 2021, I put together a blog post about how useful Spotlight data is and how it can be used to do things such as enumerate Terminal’s TCC folder permissions, search for files with keywords, and search for newly created/modified files. Here’s a link to that blog post if you want to revisit that info:

The blog above includes a link to my github repo for code examples for querying Spotlight from Swift and JXA. At the time of my blog post, I only knew how to make Spotlight API calls from Swift (my JXA projects at the time simply ran the on disk mdfind binary). However, since then I have spent more time trying to get my JXA code to more closely emulate my Swift code and I now have JXA code that queries Spotlight APIs in the same way that my Swift code does. Yay! Let’s take a look at some examples.

My previous JXA Code Calling the on-disk mdfind binary via doShellScript (undesired!):

......//runs the on-disk mdfind binary
var dir_check = currentApp.doShellScript('mdfind kMDItemKind=Folder');
//convert to an array
var dir_check2 = dir_check.split('\r');
//loop through all array values in search of TCC Protected Folders
for(let p=0; p<dir_check2.length; p++){
if (dir_check2[p] == "/Users/" + username + "/Desktop"){
results += "[+] Terminal already has folder access to /Users/" + username + "/Desktop\n";
}
if (dir_check2[p] == "/Users/" + username + "/Documents"){
results += "[+] Terminal already has folder access to /Users/" + username + "/Documents\n";
}
if (dir_check2[p] == "/Users/" + username + "/Downloads"){
results += "[+] Terminal already has folder access to /Users/" + username + "/Downloads\n";
}
}......

Swift Implementation, which gets the same data but via API calls (preferred!):

......let username = NSUserName()//set MDQuery string
let queryString = "kMDItemKind = Folder -onlyin /Users/\(username)"
let query = MDQueryCreate(kCFAllocatorDefault, queryString as CFString, nil, nil)
//run the query
MDQueryExecute(query, CFOptionFlags(kMDQuerySynchronous.rawValue))
//loop through query results
for i in 0..<MDQueryGetResultCount(query) {
if let rawPtr = MDQueryGetResultAtIndex(query, i) {let item = Unmanaged<MDItem>.fromOpaque(rawPtr).takeUnretainedValue()//grab kMDItemPath value for each entry
if let path = MDItemCopyAttribute(item, kMDItemPath) as? String {
//search for certain TCC Protected Directory Paths
if path == "/Users/\(username)/Desktop" {
print("[+] Terminal HAS ALREADY been granted TCC access to \(path)")}if path == "/Users/\(username)/Documents"{print("[+] Terminal HAS ALREADY been granted TCC access to \(path)")}if path == "/Users/\(username)/Downloads"{print("[+] Terminal HAS ALREADY been granted TCC access to \(path)")}}}}......

I then embarked on the journey of trying to figure out how to make the same API calls in JXA. I call this a “journey” because I really had a tough time finding publicly available examples or documentation related to using JXA to make MDQuery API calls. So the approach I used was to try to mimic my Swift code as closely as possible.

Results

After much trial and error, here is the working JXA code that I have which makes MDQuery API calls:

......var username = $.NSUserName().js;//set the query string for MDQuery
var queryString = "kMDItemKind = Folder -onlyin ~";
//create a new MDQuery instance
let query = $.MDQueryCreate($(), $(queryString), $(), $());
//execute the query
if ($.MDQueryExecute(query, 1)){
//loop through query results
for(var i = 0; i < $.MDQueryGetResultCount(query); i++){
var mdItem = $.MDQueryGetResultAtIndex(query, i);
//grab the kMDItemPath value for each search result
var mdAttrs1 = $.MDItemCopyAttribute($.CFMakeCollectable(mdItem), $.kMDItemPath)
var mdAttrs = ObjC.deepUnwrap(mdAttrs1);
//search for strings matching certain TCC protected dirs
if (mdAttrs == "/Users/" + username + "/Desktop"){
results += "[+] Terminal already has folder access to /Users/" + username + "/Desktop\n";
}
if (mdAttrs == "/Users/" + username + "/Documents"){
results += "[+] Terminal already has folder access to /Users/" + username + "/Documents\n";
}
if (mdAttrs == "/Users/" + username + "/Downloads"){
results += "[+] Terminal already has folder access to /Users/" + username + "/Downloads\n";
}
}
...
...

That’s it! No need to run any command line binaries!

Example of the output:

example TCC-Checker.js output

Next I went and updated all of the JXA projects in my Spotlight-Enum-Kit repository so you can see more examples (such as TCC Folder Enumeration, keyword file searches, newly created file searches, etc.) here:

--

--

Cedric Owens

Red teamer with blue team roots🤓👨🏽‍💻 Twitter: @cedowens