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
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.
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
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
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
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.
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
23/68, stop processing the recipe.
Add these items to the
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.
There are a few caveats worth mentioning:
- 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)
- 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,
--key STOP_PREDICATE=FALSEPREDICATEwill disable the StopProcessingIf behavior.
- 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:
I personally prefer maintaining a custom recipe. I most frequently run multiple
recipes using the
--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.
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.
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
Identifier org.macblog.pkg.Rectangle Input NAME Rectangle STOP_PREDICATE NOT virus_total_analyzer_summary_result.data.ratio BEGINSWITH '0' MinimumVersion 2.3 ParentRecipe com.github.dataJAR-recipes.pkg.Rectangle Process Processor io.github.hjuutilainen.VirusTotalAnalyzer/VirusTotalAnalyzer Arguments predicate %STOP_PREDICATE% Processor StopProcessingIf Processor com.github.grahampugh.jamf-upload.processors/JamfPackageUploader