Instantly share code, notes, and snippets.

Embed
What would you like to do?
package main
// https://rickt.org/2019/01/08/playing-with-g-suite-mdm-mobile-device-data-using-go/
import (
"context"
"errors"
"flag"
"fmt"
"github.com/dustin/go-humanize"
"golang.org/x/oauth2/google"
"google.golang.org/api/admin/directory/v1"
"io/ioutil"
"log"
"os"
"strings"
"time"
)
// runtime usage:
// Usage of ./mdmtool:
// -all
// List all MDM mobile devices
// -imei string
// IMEI of mobile device to search fors
// -name string
// Name of mobile device owner to search for
// -sn string
// Serial number of mobile device to search for
// -status string
// Search for mobile devices with specific status
var (
adminuser = os.Getenv("GSUITE_ADMINUSER")
companyid = os.Getenv("GSUITE_COMPANYID")
credsfile = os.Getenv("SVC_ACCOUNT_CREDS_JSON")
devices *admin.MobileDevices
row int = 0
scopes = "https://www.googleapis.com/auth/admin.directory.device.mobile.readonly"
searchtype = "all" // default search type
sortorder = "name" // default sort order
)
// flags
var (
all *bool = flag.Bool("all", false, "List all MDM mobile devices")
imei *string = flag.String("imei", "", "IMEI of mobile device to search for")
name *string = flag.String("name", "", "Name of mobile device owner to search for")
sn *string = flag.String("sn", "", "Serial number of mobile device to search for")
status *string = flag.String("status", "", "Search for mobile devices with specific status")
)
// helper func to check errors
func checkError(err error) {
if err != nil {
log.Fatal(err)
}
}
// check the command line arguments/flags
func checkFlags() (string, error) {
// parse the flags
flag.Parse()
// show all devices?
if *all == true {
// -all shows ALL devices so doesn't work with any other option
if *name != "" || *imei != "" || *sn != "" || *status != "" {
return "", errors.New("Error: -all cannot be used with any other option")
}
return "all", nil
}
// name search
if *name != "" {
// don't use -name and any other search option
if *imei != "" || *sn != "" || *status != "" {
return "", errors.New("Error: cannot use -name and any other search options")
}
return "name", nil
}
// IMEI search
if *imei != "" {
// don't use -imei and any other search option
if *name != "" || *sn != "" || *status != "" {
return "", errors.New("Error: cannot use -imei and any other search options")
}
return "imei", nil
}
// Serial number search
if *sn != "" {
// don't use -sn and any other search option
if *name != "" || *imei != "" || *status != "" {
return "", errors.New("Error: cannot use -sn and any other search options")
}
return "sn", nil
}
// Status search
if *status != "" {
// don't use -status and any other search option
if *name != "" || *imei != "" || *sn != "" {
return "", errors.New("Error: cannot use -status and any other search options")
}
return "status", nil
}
// invalid search
if *all == false && *name == "" && *imei == "" && *sn == "" && *status == "" {
flag.PrintDefaults()
return "", errors.New("Error: no search options specified")
}
return "", nil
}
// helper function to do a case-insensitive search
func ciContains(a, b string) bool {
return strings.Contains(strings.ToUpper(a), strings.ToUpper(b))
}
func main() {
// check the flags to determine type of search
searchtype, err := checkFlags()
checkError(err)
// read in the service account's JSON credentials file
creds, err := ioutil.ReadFile(credsfile)
checkError(err)
// create JWT config from the service account's JSON credentials file
jwtcfg, err := google.JWTConfigFromJSON(creds, scopes)
checkError(err)
// specify which admin user the API calls should "run as"
jwtcfg.Subject = adminuser
// make the client using our JWT config
gc, err := admin.New(jwtcfg.Client(context.Background()))
checkError(err)
// get the data
devices, err = gc.Mobiledevices.List(companyid).OrderBy(sortorder).Do()
checkError(err)
// iterate through the slice of devices
for _, device := range devices.Mobiledevices {
// what type of search are we doing?
switch searchtype {
// show all mobile devices
case "all":
row++
printDeviceData(device)
// name search: iterate through the slice of names associated with the device
case "name":
for _, username := range device.Name {
// look for the specific user
if ciContains(username, *name) {
row++
printDeviceData(device)
}
}
// IMEI search: look for a specific IMEI
case "imei":
// remove all spaces from IMEI then search for specific IMEI
// IMEI can be misreported via MDM with spaces, so remove them
if strings.Replace(device.Imei, " ", "", -1) == strings.Replace(*imei, " ", "", -1) {
row++
printDeviceData(device)
break
}
// serial number search: look for a specific serial number
// SN can be misreported via MDM with spaces, so remove them
case "sn":
if strings.Replace(device.SerialNumber, " ", "", -1) == strings.Replace(*sn, " ", "", -1) {
row++
printDeviceData(device)
break
}
// Status search
case "status":
if ciContains(device.Status, *status) {
row++
printDeviceData(device)
}
}
}
// if 0 rows returned, exit
if row == 0 {
log.Fatal("No mobile devices match specified search criteria")
} else {
// print the final/closing line of dashes
printLine()
}
fmt.Printf("%d row(s) of mobile device data returned.\n", row)
}
// func to print out device data
func printDeviceData(device *admin.MobileDevice) {
// print header only on first row of data
if row == 1 {
printHeader()
}
// convert last sync string to time.Time so we can humanize the last sync timestamp
t, err := time.Parse(time.RFC3339, device.LastSync)
checkError(err)
fmt.Printf("%-16.16s | %-14.14s | %-16.16s | %-18.18s | %-13.13s | %-18.18s | %-20.20s\n", device.Model, device.Os, strings.Replace(device.SerialNumber, " ", "", -1), strings.Replace(device.Imei, " ", "", -1), device.Status, humanize.Time(t), device.Name[0])
return
}
// func to print a line
func printLine() {
// print a line
fmt.Printf("-----------------+----------------+------------------+--------------------+---------------+--------------------+---------------\n")
}
// func to print the header
func printHeader() {
// print the first line of dashes
printLine()
// print header line
fmt.Printf("Model | OS & Version | Serial # | IMEI | Status | Last Sync | Owner\n")
// print a line of dashes under the header line
printLine()
}
// EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment