MacBlog

Stopping an AutoPkg Recipe if VirusTotal Detects Malware

Hannes Juutilainen's VirusTotalAnalyzer is a fantastic AutoPkg postprocessor. It automatically queries VirusTotal to analyze items downloaded by AutoPkg and detect potential malware.

VirusTotalAnalyzer was designed to run as a postprocessor. AutoPkg postprocessors allow you to add extra "steps" to an AutoPkg recipe at runtime without modifying the recipe itself. By this convention, VirusTotalAnalyzer scans files after all other recipe steps have completed. This means a recipe cannot conditionally act on the VirusTotal scan results; the query happens after the recipe has otherwise finished.

In practice, code signature verification, recipe trust verification, and after-the-fact VirusTotal scanning offer strong protections against malicious software. Most Mac admins also report "never" seeing VirusTotal flag a vendor package; or if they have seen it, investigation revealed a false positive.

However, many AutoPkg workflows directly upload or import software packages to a Munki repository or Jamf Pro distribution point as part of a recipe run. If VirusTotal engines flag an item, VirusTotalAnalyzer reports on the detection after the item is already uploaded to your systems. Further, most highly-automated AutoPkg workflows begin deploying the newly-uploaded software to a test group (or all endpoints) as soon as the recipe completes.

You may require additional assurance that downloaded software is not flagged by VirusTotal, and want to prevent any flagged files from being uploaded to your software distribution points.

You can do this by using a custom recipe that runs VirusTotalAnalyzer as a regular processor – instead of as a postprocessor – combined with the StopProcessingIf processor. This allows you to terminate a recipe if VirusTotal reports any hits before subsequent recipe steps upload an item to your systems.

Here's how to do that.

Create a Custom Recipe

While it's possible to append additional processors to a recipe within a recipe override, or call multiple postprocessors on the command line, I recommend creating a custom recipe for each application. This allows you to very specifically define exactly what you wish to happen during a recipe run.

Yes, this is additional overhead. However, it's not uncommon for an organization to maintain custom recipes for all software to meet automation needs. And hey, if you want this extra security layer, you'll have to work a little for it.

I recommend creating an organization-specific child recipe for each app in your catalog, and adding the required processors to that child recipe.

Let's use Rectangle as an example. Here's the shell of a new custom recipe:

Identifier: org.macblog.pkg.Rectangle
ParentRecipe: com.github.dataJAR-recipes.pkg.Rectangle
MinimumVersion: "2.3"
Input:
  NAME: Rectangle
Process:

Here, I've set a new Identifier for the recipe specific to my organization. In this example, I ultimately want a .pkg, so I set the ParentRecipe as the identifier of the pkg version of the Rectangle recipe. Finally, since I'm using YAML here, I set the MinimumVersion to 2.3 to require the version of AutoPkg that supports YAML-formatted recipes.

I'm setting the NAME key in the Input dictionary to ensure AutoPkg recognizes the file as a valid recipe. No additional Input keys are required for this minimal example. You can, however, customize them to fit your needs.

Since this is a child recipe it inherits all the output of the parent recipe. That means we don't need to do anything to get the pkg output from the parent, and the child recipe only needs to contain the additional processors we want to run.

Flow chart illustrating parent and child AutoPkg recipes.
Each child recipe receives and can act on the output of its parent recipe.

Add VirusTotalAnalyzer as a Regular Processor

Instead of running VirusTotalAnalyzer as a postprocessor, you can call it as a "regular" processor inside the Process dictionary of our custom recipe. Add it like so:

    
Identifier: org.macblog.pkg.Rectangle
ParentRecipe: com.github.dataJAR-recipes.pkg.Rectangle
MinimumVersion: "2.3"
Input:
  NAME: Rectangle
Process:
  - Processor: io.github.hjuutilainen.VirusTotalAnalyzer/VirusTotalAnalyzer
    

Since processors within a recipe run sequentially, you can run VirusTotalAnalyzer immediately after downloading an item, but before subsequent processors upload that item to your systems. It requires only that a pathname variable exists.

In this example, the parent recipes have already handled downloading and packaging the app. Our child recipe receives all the output of those previous actions, and we're ready to use VirusTotalAnalyzer to scan the downloaded item for threats.

Add a StopProcessingIf Processor

Next, add the StopProcessingIf processor. The StopProcessingIf processor terminates a recipe execution if a condition is met. This is most commonly used to stop processing a recipe if a vendor download has not changed, by setting the predicate to download_changed == FALSE.

Because StopProcessingIf accepts NSPredicate-style conditions, we can also test more complex conditions.

VirusTotalAnalyzer outputs its findings to the virus_total_analyzer_summary_result output variable. Within that output is a ratio key that denotes the number of VirusTotal engines that flagged a file as suspicious out of the total number of engines that scanned the file, e.g. 0/55.

We can use the NSPredicate test BEGINSWITH to target the beginning of the ratio variable string. That variable is nested a couple levels deep, and we use dot notation to access nested values. Combined with a NOT negation, our full predicate is:

NOT virus_total_analyzer_summary_result.data.ratio BEGINSWITH '0/'

This means: If the reported ratio from VirusTotal begins with anything other than 0/, e.g. 1/54 or 23/68, stop processing the recipe.

Add these items to the Input and Process dictionaries like so:

    
Identifier: org.macblog.pkg.Rectangle
ParentRecipe: com.github.dataJAR-recipes.pkg.Rectangle
MinimumVersion: "2.3"
Input:
  NAME: Rectangle
  STOP_PREDICATE: "NOT virus_total_analyzer_summary_result.data.ratio BEGINSWITH '0'"  
Process:
  - Processor: io.github.hjuutilainen.VirusTotalAnalyzer/VirusTotalAnalyzer

  - Processor: StopProcessingIf
    Arguments:
      predicate: "%STOP_PREDICATE%"
    

Setting the predicate as an Input key allows you to override the predicate at recipe runtime if you need to temporarily skip the StopProcessingIf functionality. Manually passing --key STOP_PREDICATE=FALSEPREDICATE will bypass the StopProcessingIf processor and allow the recipe to complete, even if VirusTotal reports a detection.

Update: Thanks to Graham Pugh for the suggestion to specify the predicate in the Input dictionary.

Add Your Other Processors

Add any additional processors after the StopProcessingIf processor to suit your needs.

You might add JamfUploader or MunkiImporter processors to copy a package to your distribution servers. Whatever you add after the StopProcessingIf processor will be executed only if VirusTotal reports no malware detections.

This custom recipe can be used just like any other. You can (and should!) make local overrides, run it in recipe lists, add chat-notifying postprocessors, and commit it to your organization's private recipe repository. Run it manually, or within AutoPkgr, or in your CI pipeline.

Final Thoughts

There are a few caveats worth mentioning:

  1. You need to create a custom recipe for every app in your catalog. You may be doing this already – if so, good for you! (Or, try this alternative)
  2. You might get false positives, where a single VirusTotal engine erroneously reports a detection. You'll need to follow up manually, and perhaps temporarily disable the VirusTotalAnalyzer (or StopProcessingIf) processor in a recipe until VirusTotal or the vendor fixes the issue. As mentioned, passing --key STOP_PREDICATE=FALSEPREDICATE will disable the StopProcessingIf behavior.
  3. You cannot set a "threshold" of a permissible number of potential detections. The predicate used in the StopProcessingIf processor allows only zero detections.

If you don't mind this overhead, and your organization requires this increased assurance, you fit within the narrow band of utility this process provides. You might even view the above caveats as good things. If so, enjoy!

Alternative to a Custom Recipe

AutoPkg is flexible, and you can run multiple postprocessors sequentially. Instead of creating a custom recipe, it's possible to "append" the required postprocessors to an existing recipe on the command line as follows:

autopkg run com.github.dataJAR-recipes.pkg.Rectangle \
--post io.github.hjuutilainen.VirusTotalAnalyzer/VirusTotalAnalyzer \
--post StopProcessingIf \
--key predicate="NOT virus_total_analyzer_summary_result.data.ratio BEGINSWITH '0'" \
--post com.github.grahampugh.jamf-upload.processors/JamfPackageUploader

I personally prefer maintaining a custom recipe. I most frequently run multiple recipes using the -l or --recipe-list flag, and adding postprocessors to a recipe list run will call all those postprocessors for every recipe. For me, that decreases the flexibility afforded by creating a very explicit custom recipe, which is why I recommend that route.

But the multiple-postprocessor option works fine if you prefer it.

Complete Examples

Here is a complete recipe example in both YAML and XML formats. It demonstrates uploading the example Rectangle package to Jamf Pro if VirusTotal engines find no malware.

YAML

Identifier: org.macblog.pkg.Rectangle
ParentRecipe: com.github.dataJAR-recipes.pkg.Rectangle
MinimumVersion: "2.3"
Input:
  NAME: Rectangle
  STOP_PREDICATE: "NOT virus_total_analyzer_summary_result.data.ratio BEGINSWITH '0'"
Process:
  - Processor: io.github.hjuutilainen.VirusTotalAnalyzer/VirusTotalAnalyzer

  - Processor: StopProcessingIf
    Arguments:
      predicate: "%STOP_PREDICATE%"

  - Processor: com.github.grahampugh.jamf-upload.processors/JamfPackageUploader

XML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Identifier</key>
  <string>org.macblog.pkg.Rectangle</string>
  <key>Input</key>
  <dict>
    <key>NAME</key>
    <string>Rectangle</string>
    <key>STOP_PREDICATE</key>
    <string>NOT virus_total_analyzer_summary_result.data.ratio BEGINSWITH '0'</string>
  </dict>
  <key>MinimumVersion</key>
  <string>2.3</string>
  <key>ParentRecipe</key>
  <string>com.github.dataJAR-recipes.pkg.Rectangle</string>
  <key>Process</key>
  <array>
    <dict>
      <key>Processor</key>
      <string>io.github.hjuutilainen.VirusTotalAnalyzer/VirusTotalAnalyzer</string>
    </dict>
    <dict>
      <key>Arguments</key>
      <dict>
        <key>predicate</key>
        <string>%STOP_PREDICATE%</string>
      </dict>
      <key>Processor</key>
      <string>StopProcessingIf</string>
    </dict>
    <dict>
      <key>Processor</key>
      <string>com.github.grahampugh.jamf-upload.processors/JamfPackageUploader</string>
    </dict>
  </array>
</dict>
</plist>

Using JXA in Jamf Pro Scripts and Extension Attributes

Quick follow-up to my earlier guide on using JavaScript for Automation.

There must be something in the air that put the topic of JXA on the minds of the Apple community. Armin Briegel shared a great roundup of recent JXA work, and the #scripting channel on the MacAdmins Slack team is full of folks discussing new and old discoveries.

Here are a handful of additional tips.

Excutable JXA scripts

You can run JXA directly by including the JavaScript language flag in a script's shebang, like this:

#!/usr/bin/osascript -l JavaScript

function run() {
	var app = Application.currentApplication();
	app.includeStandardAdditions = true;
	return app.systemInfo().cpuType;
}

Save the file with a name and extension you like. I've been using .scpt, which is the convention suggested by Apple's own Script Editor application. Something like cputype.scpt will work just fine.

Mark the file as executable by running chmod +x cputype.scpt. Then, you can run it on your system by simply calling ./cputype.scpt

This will output the CPU architecture of the computer, for example ARM64E or Intel x86-64...

JXA in Jamf Pro Scripts

You can drop a JXA script directly in Jamf Pro without any modification. As long as you include the JavaScript shebang outlined above, you're good to go.

You do not necessarily need to wrap JXA in a shell script. It might make sense to "shell out" to a JXA function from a shell script for some use cases, but it is not a requirement.

Jamf supports non-compiled AppleScript files natively, and you can use them in your Policies like any other script.

JXA in Jamf Pro Extension Attributes

Same deal; specify the Script type for the Extension Attribute, and include the JavaScript language flag shebang. Ensure your function returns the required <result></result> wrapper surrounding the output. You can do that with simple string concatenation using the plus operator, like this:

#!/usr/bin/osascript -l JavaScript

function run() {
	var app = Application.currentApplication();
	app.includeStandardAdditions = true;
	return "<result>" + app.systemInfo().cpuType + "</result>";
}

This will output <result>ARM64E</result>, for example, which will show up on your computer's inventory record during the next device inventory.

--

That's it. Hopefully this helps you store and run JavaScript for Automation scripts.


How to Parse JSON on the macOS Command Line Without External Tools Using JavaScript for Automation

JSON – JavaScript Object Notation – is the lingua franca for shipping data between systems. Everything from software APIs to web services commonly support, and typically default to, outputting data in JSON format.

Because of its ubiquity, you're bound to run into a need to manipulate a chunk of JSON in the course of managing your fleet.

For example, you might run a shell script on your Macs that instructs them to read data from an external system via its API using curl. That external system returns a big ol' heap of JSON, but you need to extract only a single data point from that payload, then act on that value.

This is somewhat of a challenge in the shell because macOS does not include any native or pre-installed tools to help you hack down that JSON. Administrators often resort to workarounds like shelling out to Python or using sed and awk to approximate parsing.

However, another, native solution is right there in the name: use JavaScript.


How to Disable iCloud Private Relay in macOS Monterey

Apple recently introduced iCloud Private Relay as an additional benefit for iCloud+ subscribers. The feature routes Safari web browsing (and some other insecure Internet traffic) through a semi-anonymizing service to reduce third parties' ability to profile and track individual users.

However, it may be necessary in some environments to disable iCloud Private Relay. The feature may interfere with management controls, prevent required traffic auditing, or complicate troubleshooting procedures.

Apple provides a guide to prepare your network or service for iCloud Private Relay, but it's also possible to disable the feature using a Restrictions Configuration Profile.