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