Unquarantine Mac Apps

On every Mac I manage – including my personal Mac I’m using right now – I use https://brew.sh to manage my installed apps. But – there’s one thing I truly despise – and that is running brew as my logged-in, low-privilege, non-sudo user. So I run brew as a different, privileged user. (And – of course – you run the same?)

Oh wait – “brew is not supported in a multi-user mode! It could break any INSTANT!”. Yes, I’ve actually had that complaint made. What a bunch of FUD. Please – pay no attention to the naysayers – I’ve run brew as a separate privileged user to manage a whole lab of Macs. For many years. With almost zero problems. (Yes there are a few – but very few – problems. To be covered later in more detail later.)

One problem is that brew casks installed as the separate user continuously prompt the user about “Unsafe program downloaded from Internet – Do you want to open?” Which of course is meaningless to the user. And since the “unquarantine” only applies if *the current user* is the same user who ran brew to install the cask – the problem Never Goes Away. So I want to fix that problem – and here is how.

I use Chef to manage all of my Macs – so one of the steps is to install all the required casks, and then another is to run the script to unquarantine those casks. And for you, dear reader, here is that awesome script. Enjoy!


#!/bin/bash

# save my path
THE_PATH="$0"
THE_CUR_USER="$(whoami)"

# locate sed (latest brew installs only to gsed)
the_sed=$(which gsed 2>/dev/null)
[ x"$the_sed" = x ] && the_sed=$(which sed)
#echo "Using '$the_sed'"

# locate the chef-user
the_chef_user=$(ls -ld $(which brew) | awk '{print $3}')

# these are in here to allow this script to work directly in chef recipe
fool_ruby_parser='\'
fool_ruby_parser+='1'

# are we the current chef user?
if [ x"$the_chef_user" != x"$THE_CUR_USER" ] ; then
  # run command as different user
  echo "Enter password for '$the_chef_user' when prompted..."
  the_rc=1
  while [ $the_rc -ne 0 ] ; do
    su $the_chef_user -c "$THE_PATH"
    the_rc=$?
    [ $the_rc -ne 0 ] && echo 'Try again...'
  done
	exit $the_rc
fi

# iterate over all installed casks
for i in $(sudo -u $the_chef_user brew cask list) ; do
  echo -n "Unquarantine $i: "
  the_app_path=''
  the_dir=$(find /usr/local/Caskroom -name $i -type d 2>/dev/null)
  l_rc=$? ; [ $l_rc -ne 0 ] && echo "find /usr/local/Caskroom fail with $l_rc" && continue
  [ x"$the_dir" = x ] && echo 'empty the_dir' && continue

  # are there artifacts available?
  if sudo -u $the_chef_user brew cask info $i 2>/dev/null | grep -ie '^==> Artifacts' >/dev/null 2>&1 ; then
    # *must* have an app
    the_app_name=$(sudo -u $the_chef_user brew cask info $i | grep -A10 -ie '^==> Artifacts' | grep -ie '(App)' | $the_sed -e "s/^\(.*\) \+(App)/$fool_ruby_parser/i")
    [ x"$the_app_name" != x ] && the_app_path="/Applications/$the_app_name"
  fi

  # if no app lookup from install file
  if [ x"$the_app_path" = x ] ; then
    # get the install file first
    the_installer_file=$(sudo -u $the_chef_user brew cask info $i | grep -ie '^from: ' | $the_sed -e "s/^from: \+\(.*\)/$fool_ruby_parser/i; s/^.*\/\([^\/]\+\)\$/$fool_ruby_parser/" | head -n 1)
    l_rc=$? ; [ $l_rc -ne 0 ] && echo "brew cask info fail with $l_rc" && continue
    [ x"$the_installer_file" = x ] && echo 'empty the_installer_file' && continue

    # where is it in brew?
    the_installer_path=$(find "$the_dir" -type f -name "$the_installer_file" 2>/dev/null | head -n 1)
    l_rc=$? ; [ $l_rc -ne 0 ] && echo "find $the_dir fail with $l_rc" && continue
    [ x"$the_installer_path" = x ] && echo "empty the_installer_path" && continue

    # extract the app name
    the_app_path=$(cat "$the_installer_path" | grep -e '/Applications/' | $the_sed -e "s/^.*\(\/Applications\/.*\.app\).*/$fool_ruby_parser/")
    l_rc=$? ; [ $l_rc -ne 0 ] && echo "grep /Applications/ fail with $l_rc" && continue
  fi
  [ x"$the_app_path" = x ] && echo 'empty the_app_path' && continue

  # app must be a valid name
  [ ! -d "$the_app_path" ] && echo "missing app '$the_app_path'" && continue

  # disable the quarantine bit on the discovered application
  sudo xattr -r -d com.apple.quarantine "$the_app_path"
  l_rc=$? ; [ $l_rc -ne 0 ] && echo "xattr fail $l_rc" && continue
  echo 'OK'
done

Let’s give a few notes before I move on to my next project 😉

  • One problem I ran into when deploying this script from Chef was that I wanted to use ERB so I could do substitutions if I ever needed to. But my regex failed because the Chef parser kept killing my \1 replacement references. So I have a trick or two to fool the Chef ruby parser. Just FYI.
  • Not every app will have an actual app_path I can deterministically identify. Here’s an actual output:
    
    $ ~/bin/lcl-disable-brew-quarantine-bit.sh
    Enter password for 'none-of-your-beeswax' when prompted...
    Password:
    Unquarantine docker: Password:
    OK
    Unquarantine firefox: OK
    Unquarantine gimp: OK
    Unquarantine google-chrome: OK
    Unquarantine idrive: empty the_app_path
    [...]
    
    Thus far I haven’t found this to be a problem – all the applications that my users *can* open interactively are properly unquarantined.
  • I use the term chef-user in the code above to identify the user which owns brew. So, yes, that could be “brew-user” instead, but in my case since all is managed using Chef – I haven’t modified it for this post.

Happy Computing!

Team-oriented systems mentor with deep knowledge of numerous software methodologies, technologies, languages, and operating systems. Excited about turning emerging technology into working production-ready systems. Focused on moving software teams to a higher level of world-class application development. Specialties:Software analysis and development...Product management through the entire lifecycle...Discrete product integration specialist!

Tagged with:

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Human Verification: In order to verify that you are a human and not a spam bot, please enter the answer into the following box below based on the instructions contained in the graphic.