|
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 |