[This documentation, and the utility itself, are currently in a rudimentary state: caveat emptor.]
This is a primitive utility which allows you to ‘warrant’ certain commands using one or more configuration files. It’s like a tiny version of
sudo except without the implication of SUID or most of the complexity, and probably without most of the security either.
So what’s it useful for? MacOS has recently developed a complicated layer of privacy controls which mean that programs (not just applications with GUIs: anything) which want to access data such as contacts &c need permission to do so. This access control is completely orthogonal to the Unix filesystem permissions.
This is fine … unless you want to run periodic background jobs which run scripts or other programs in order to, for instance, make backups: if you try to run anything which walks over your home directory then it will either fail altogether or pop up dialogue boxes at odd times, which is not the behaviour you want from cron jobs.
There is a solution to this: you can grant specified programs ‘full disk access’ which allows them to bypass all this. There is some mechanism I don’t understand which decides which program needs to have that access: for instance if you sit in a terminal window and type
$ find ~ -type f -print > /dev/null
Then the dialogue which pops up will ask you whether you want to grant access to
iTerm if you are sensible and use that), not
/usr/bin/find. It seems to be doing some magic which allows it to walk up the process tree and find the closest thing with a GUI to ask about (and in fact it may be able to do some hairy thing to find the appropriate thing even if it’s not an ancestor of the process which actually needs access).
In the case of programs where there is no GUI component such as cron jobs, it does seem to be enough to grant permission to something up the process tree. So one solution would be to ‘bless’ (allow full disk access to)
/usr/sbin/cron. But that’s tantamount to blessing all commands, as you can put anything in a
crontab. And, worse, you might not even be using
cron: I use
launchd, and I really don’t want to bless that, since almost everything uses it to run things.
So you need a wrapper of some kind which you can bless. One obvious approach would be to bless
sudo, but I didn’t want to do that as it would mean I would need to maintain a
sudoers file which might or might not get unilaterally overwritten by the system’s one, and also there might be other things in it which I don’t want to be blessed.
warranted: this reads one or more files with command descriptions in them, and checks a command line against it, running it if it matches. If you install it in
/usr/local/bin, then you can bless it (remember ‘CMD-shift-.’ to let the Finder see all files), and use it to run commands you are interested in.
All configuration file syntax is standard Racket syntax: everything is read with
read wrapped with
call-with-default-reading-parameterization and with the
read-accept-reader parameters false. There is exactly one form in any file (you can add others, but only the first will be read).
There are two sets of files:
Meta files are searched for in a fixed set of locations in a fixed order and the first one found is read, only. The fixed set of places is, in order:
~meaning ‘user’s home directory’ un the usual way).
If any of these files is found it should contain a single list of filenames: these are the command specification files to read. So to make
warranted be really fussy install a meta file in
/etc/warranted/meta.rktd and put in that a single file or files which can be carefully controlled. For instance the contents of
/etc/warranted/meta.rktd might be
which will cause
warranted to look only at
/etc/warranted/commands.rktd for command specifications.
Command specification files are looked for either wherever the meta file tells it to look or in a set of standard places:
All the files which are found are read, and they are combined into a single specification specifying what can be read.
Each command specification file contains a single form, which is a list of command specifications.
A command line matches a command specification if all its elements match.
PATHand this is what is matched. So, for instance
catis turned into
/bin/catand this must be what is in the command specification.
*matches any single entry in the command line.
**matches zero or more entries in the command line.
/matches no elements in the command line (see below for why this is useful).
These examples are of single command specifications: remember that a specification file contains a list of command specifications. Note that not all of the command specifications below make any sense, or are safe: they’re just here as examples.
("/bin/ls") will match
ls with no arguments only (assuming that
type -p ls is
/bin/ls which it is by default).
("/bin/ls" "/etc/motd") will match
ls /etc/motd only.
("/bin/ls" *) will match
ls with any single argument. Note that
warranted has no idea whether an argument is a switch or not.
("/bin/ls" "-l" *) will match
ls -l and any other single argument.
("/bin/ls" **) will match
ls with any number of arguments, including zero.
("/bin/ls" ** "/etc/motd") would match
ls with any number of arguments so long as the last one is
("/bin/ls" ("/etc/motd" "/etc/hostname")) will match
ls /etc/motd or
ls /etc/hostname, only.
("/bin/ls" (/ "-l") "/etc/motd") matches
ls /etc/motd and
ls -l /etc/motd. This works because the disjunction either matches
-l or matches nothing with
/: this is what
/ is useful for.
("/bin/ls" (/ ("-l" "-r")) "/etc/motd") matches either
ls /etc/motd or
ls -l -r /etc/motd, which it does because the disjunction contains a nested command specification.
Finally here is the content of my
(["/Users/tfb/lib/cron/run" ;; My cron commands (/ "-t") ("hourly" "nightly" "weekly" "monthly" "yearly")])
Note that I’ve used
[...] which Racket allows, and that this warrants, for instance
/Users/tfb/lib/cron/run hourly and
/Users/tfb/lib/cron/run -t yearly but not
/Users/tfb/lib/cron/run -x annually.
There are various options not described here (but
warranted -h will give you some hints). The basic usage is
$ warranted command [arg ...]
which, if the executable corresponding to
command is warranted with the given arguments, will run it & return its exit code. If it’s not warranted, then
warranted returns an exit code of
So, I have
launchd) jobs which run
/usr/local/bin/warranted $HOME/lib/cron/run hourly
One option which is useful is
warranted -n ... will tell you whether it would run the specified command but not actually run it:
$ warranted -n ls unwarranted command line ("/bin/ls") (from ("ls")) $ warranted -n ~/lib/cron/run hourly [would run ("/Users/tfb/lib/cron/run" "hourly")] $ echo $? 0 $ warranted -n ~/lib/cron/run annually unwarranted command line ("/Users/tfb/lib/cron/run" "annually") (from ("/Users/tfb/lib/cron/run" "annually")) $ echo $? 1
The main purpose of the thing is to evade the MacOS privacy controls without just giving up completely: it’s not intended as a comprehensive solution to anything and in particular
I make no promise at all that
warranted is even slightly secure: you use it entirely at your own risk.
Specific security notes.
warrantedis only as secure as the Racket reader, which is probably not very secure.
If you care about security, read the code: it’s not very big.
warranted is written in Racket, and has been built with Racket 7.2 & 7.3. You will need a Racket installation with
raco in your
PATH, & a working
make (the Xcode one is fine). The
raco exe to make a binary and then
raco distribute to make a distribution which should not depend on the installed Racket. The default target just makes the binary,
make distrubution will make the distribution directory, and
make install will try to install the distribution. Installing is fiddly in the usual way if the user who can run
raco is not the one who can write into the install directory.
Although this is not apparent from the GUI, it seems to be the case that when you bless a locally-built executable, such as
warranted, you are blessing the specific file: presumably it works out whether the file is blessed or not by computing some signature of it and comparing it with what it knows. This means that if you rebuild & reinstall it it will stop being blessed, although the GUI will show that it still is. So each time you reinstall
warranted you have to do a little dance to remove the old one from the list and then bless the new one. The answer to this is probably code signing, but I have no idea how that works: I suspect the answer is here.
Copyright 2019, 2020 Tim Bradshaw
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.