How to Use Munki Conditions With Osquery
A common capability sought after by MacAdmins, is a method to make changes or install software on a specific subset of Macs. In some cases, this subset is static and easily defined; in other cases, installation of new software hinges on a dynamic property of the device (eg. its physical location when installing printers). Ideally, these dynamic properties could be monitored and used as conditional triggers for the installation of software.
To address this issue at Kolide, we use the open-source tool Munki
(from Walt Disney Animation Studios). Munki works seamlessly with just about
any Mac “installable” type there is and features a number of powerful
customization options. Among these options, is a feature called
Conditional Items
which allows us to install or remove software if
certain conditions are met on a Mac.
Remember our printer example? In order to automatically install the appropriate
printers, a Conditional Item
could be created which looked at the LAN_subnet
on the device. This way an employee traveling between different offices would
get the correct set of printers at each location.
This dynamic approach to software management is terrifically powerful, but
initially limited by the small number of pre-built conditions that ship with
Munki (hostname
, os_vers
, machine_model
and serial_number
).
Thankfully however, there is a mechanism for administrators to create custom
conditions and we can use tools like Osquery to
radically expand what’s possible.
To create a new conditional item, an administrator writes a script which
creates a key/value pair(or array of values) and saves it in
/Library/Managed Installs/ConditionalItems.plist
. Usually that’s a
bash
or python
script to query some part of the system and create a new
condition. But, if we already have an osquery process running, we can gain
access to a wide variety of facts about the Mac with the power of a simple SQL
query. Allister Banks, a fellow MacAdmin and
osquery user suggested I could write this integration with using the
osquery-python bindings,
but we were developing osquery-go
,
so I thought it would be a good opportunity to test the Go API.
I wanted to take the opportunity here and show Munki users how they can enhance
their Munki configuration with osquery, and also provide a hands-on
introduction to osquery-go
,
our Go SDK for writing plugins and interacting with the osquery daemon.
Let’s dive in!
Creating the ConditionalItems Plist
The Munki documentation says:
Each “write” to ConditionalItems.plist must contain a key/value pair. A value may also be an ‘array’ of values, similar to the built-in conditions ‘ipv4_address’ or ‘catalogs’.
In Go we could represent this data structure with a new type.
type MunkiConditions map[string][]string
We will also need to read and write the plist file, so let’s create these as helper methods.
Note: To keep the blog post short and readable I will abbreviate some of the code samples. The full implementation is linked at the end of the post.
func (c *MunkiConditions) Load() error { ... }
func (c *MunkiConditions) Save() error { ... }
Interfacing With Osquery
Osquery provides a powerful API for external plugins using Apache Thrift. The Thrift API can be used to implement custom loggers, tables, configuration plugins or to run queries. Today we’ll explore how to call the Query method using the osquery-go client.
First, we need a to create a client.
// The path to the osquery unix domain socket.
socketPath := "/var/osquery/osquery.em"
// Create a client, with a 10 second timeout in case the socket is not available.
client, err := osquery.NewClient(socketPath, 10*time.Second)
The returned client looks like this:
type ExtensionManagerClient struct { ... }
func (p *ExtensionManagerClient) Query(sql string) (r *ExtensionResponse, err error)
So now we could run:
extensionResponse, err := client.Query("select version from osquery_info;")
response := extensionResponse.GetResponse()
Which will return the following type:
type ExtensionPluginResponse []map[string]string
The map corresponds to an array of osquery table output.
Converting ExtensionPluginResponse to MunkiConditions
Now that we know how to update the ConditionalItems.plist
file and run
queries programmatically, all we need is some glue code to convert the
ExtensionPluginResponse
type into the MunkiConditions
type.
conditions := make(MunkiConditions)
for row := range response {
for key, value := range row {
// add osquery_ suffix to avoid namespace collisions
conditions[fmt.Sprintf("osquery_%s", key)] = []string{value}
}
}
Run It Concurrently
You probably want to run many queries to generate the ConditionalItems file. Unfortunately the Thrift API won’t run the queries in parallel, but we can at least use Go’s concurrency features to schedule all our queries asynchronously.
// OsqueryClient wraps the extension client.
type OsqueryClient struct {
*osquery.ExtensionManagerClient
}
// RunQueries takes one or more SQL queries and returns a channel with all the responses.
func (c *OsqueryClient) RunQueries(queries ...string) (<-chan map[string]string, error) {
responses := make(chan map[string]string)
// schedule the queries in a separate goroutine
// it doesn't wait for the responses to return.
go func() {
for _, q := range queries {
resp, err := c.Query(q)
if err != nil {
log.Println(err)
return
}
if resp.Status.Code != 0 {
log.Printf("got status %d\n", resp.Status.Code)
return
}
for _, r := range resp.Response {
responses <- r
}
}
// close the response channel when all queries finish running.
close(responses)
}()
return responses, nil
}
Putting It All Together
To write our conditions script we can do the following:
var conditions MunkiConditions
err := conditions.Load()
extensionClient, err := osquery.NewClient(socketPath, 10*time.Second)
defer client.Close()
client := &OsqueryClient{extensionClient}
responses, err := client.RunQueries(...)
for row := range response {
for key, value := range row {
conditions[fmt.Sprintf("osquery_%s", key)] = []string{value}
}
}
err := conditions.Save()
You can find the full implementation of the osquery-condition utility at https://github.com/groob/osquery-condition
If you’d like to read more osquery content like this, sign up for our biweekly newsletter.