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.
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:
- 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,
passing
--key STOP_PREDICATE=FALSEPREDICATE
will 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 -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
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