top of page

PowerShell on macOS with AI: Graph Queries a Mac Admin Actually Runs

Writer: MacSmithAI
MacSmithAI
Sep 1
7 min read

Updated: 4 days ago

If you manage Macs with Intune, sooner or later you need an answer the console won't hand you in a usable shape. How many of our Macs are still on Sonoma? Which ones haven't checked in since the last OS push? Which serials belong to the noncompliant machines in one org unit? The Intune admin center will show you some of that, five clicks deep, in a table you can't pipe anywhere. Microsoft Graph will give you all of it in one line — if you can write the line.


That's where a bash-native admin stalls. Not on the concepts; you already know what a device record is and what you want to filter on. You stall on PowerShell's verb-noun grammar, on the fact that objects come out of the pipe instead of text, and on a module surface with hundreds of cmdlets whose names you'd never guess. This is exactly the gap an AI assistant closes well — and exactly the place it will hand you a confident, well-formatted cmdlet that does not exist. Here's the setup that works on a Mac, the queries worth keeping, and the verification habit that makes AI-written PowerShell safe to run against a real tenant.


Get PowerShell onto the Mac first

PowerShell 7.6 is the current LTS, released 18 March 2026 and supported through 14 November 2028. It runs on macOS 26 (Tahoe), 15 (Sequoia), and 14 (Sonoma). Worth noting if you're still on an older build: PowerShell 7.4 LTS and 7.5 both go out of support on 10 November 2026, so 7.6 is the version to standardize on.


Homebrew works and is the fastest path on your own machine:


brew install powershell

But read the fine print before you push this to a fleet — Microsoft's own docs say the brew formula is maintained by the Homebrew community, builds from source rather than installing a Microsoft-built package, and is not officially supported by Microsoft. For anything you're deploying through Intune, take the signed .pkg from Microsoft instead and wrap it like any other package. Keep brew for your admin workstation if you like the convenience, but don't let an unsupported build become the thing your compliance reporting depends on.


Install less than you think

The obvious move is Install-Module Microsoft.Graph, and it will work:


Install-Module Microsoft.Graph -Scope CurrentUser -Repository PSGallery -Force

It also pulls more than 47 sub-modules, and Microsoft's documentation openly suggests installing only the ones you need. For fleet reporting, that's two:


Install-Module Microsoft.Graph.Authentication  -Scope CurrentUser -Repository PSGallery
Install-Module Microsoft.Graph.DeviceManagement -Scope CurrentUser -Repository PSGallery

Microsoft.Graph.Authentication is the dependency everything else sits on, and it comes along automatically when you install sub-modules individually. Only cmdlets from installed modules are available, so if an AI hands you something from Microsoft.Graph.Identity.SignIns and PowerShell says it can't find it, the cmdlet may be real and simply not installed. Find-Module Microsoft.Graph* lists what's out there; Update-Module Microsoft.Graph keeps it current.


Signing in from a Mac

Interactive is one line, and you ask for the narrowest scope that answers your question. Listing Intune devices needs DeviceManagementManagedDevices.Read.All (or the ReadWrite variant, which you don't want for reporting):


Connect-MgGraph -Scopes "DeviceManagementManagedDevices.Read.All"

If the browser handoff misbehaves — SSH session, locked-down profile, kiosk-ish setup — Connect-MgGraph -Scopes "..." -UseDeviceAuthentication gives you the device-code flow instead.


App-only auth is where macOS gets interesting. Microsoft's certificate examples for -CertificateThumbprint and -CertificateName read from Cert:\LocalMachine\My\, a Windows-only provider path. On a Mac, plan on -ClientSecretCredential with a tenant ID, or on loading the certificate yourself and passing the object to -Certificate. Whichever you pick, prove it works interactively before you build an unattended pipeline on top of it — this is the step that quietly breaks when a script written on a Windows box gets moved to a Mac.


Four queries worth keeping

Start with the one nobody tells you to run first: find out what your tenant actually calls a Mac. The operatingSystem property is a free-text string that Microsoft documents only as "Windows, iOS, etc.", casing is not something to guess at, and a filter with the wrong string returns a cheerful empty set rather than an error.


# 1. What does this tenant call each platform?
Get-MgDeviceManagementManagedDevice -All |
  Group-Object OperatingSystem |
  Select-Object Name, Count

Use whatever string that returns in everything below. Then the inventory pull:


# 2. Every Mac, in the fields you'd actually put in a spreadsheet
$macs = Get-MgDeviceManagementManagedDevice -All -Filter "operatingSystem eq 'macOS'" `
    -Property DeviceName,SerialNumber,OSVersion,ComplianceState,LastSyncDateTime,UserPrincipalName

$macs | Select-Object DeviceName,SerialNumber,OSVersion,ComplianceState,LastSyncDateTime |
    Export-Csv ~/Desktop/macs.csv -NoTypeInformation

-Property is the cmdlet's name for OData $select (its alias is literally -Select), and -All handles paging for you — the thing you'd otherwise be chasing @odata.nextLink for by hand.


# 3. Noncompliant Macs
Get-MgDeviceManagementManagedDevice -All -Filter "complianceState eq 'noncompliant'" |
    Where-Object OperatingSystem -eq 'macOS' |
    Select-Object DeviceName, UserPrincipalName, OSVersion

# 4. Stale check-ins — the machines that dropped off
$cutoff = (Get-Date).AddDays(-30).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")

Get-MgDeviceManagementManagedDevice -All -Filter "lastSyncDateTime lt $cutoff" |
    Where-Object OperatingSystem -eq 'macOS' |
    Sort-Object LastSyncDateTime |
    Select-Object DeviceName, UserPrincipalName, LastSyncDateTime

Notice the split: server-side -Filter on one property, then Where-Object for the rest. That's deliberate. Graph documents $filter support per property — complianceState supports eq and or, lastSyncDateTime supports lt and gt — and the support is narrower than OData in general. The in operator on operatingSystem has produced a 500 Internal Server Error against this endpoint, filed as a service issue with no documented workaround. When a filter throws a 500, don't assume you wrote it wrong. Simplify the server-side filter and finish the job in the pipeline.


Three ways to ask Graph the same question

| | Get-Mg* cmdlets | Invoke-MgGraphRequest | curl + jq | |---|---|---|---| | Best at | Everyday reporting; typed objects straight into Select-Object / Export-Csv | Endpoints with no cmdlet yet, or when you want the raw JSON shape | Dropping a Graph call into an existing bash script | | Auth | Connect-MgGraph handles tokens and refresh | Reuses the same session | You acquire and refresh the token yourself | | Paging | -All | You follow @odata.nextLink | You follow @odata.nextLink | | Beta endpoints | Separate Microsoft.Graph.Beta.* module and Get-MgBeta* cmdlets | Change the URL | Change the URL | | Where it bites | Cmdlet sprawl; PascalCase property names don't match the JSON you see in docs | Output isn't a typed device object, so the pipeline is clumsier | Token handling and paging are yours forever |


For fleet reporting, the cmdlets win — not because they're elegant, but because -All and typed output remove the two things that make hand-rolled Graph scripts rot. Keep Invoke-MgGraphRequest in your pocket for the beta-only endpoint that has no cmdlet yet.


Where AI helps, and where it invents things

AI is genuinely good at the part you find annoying: translating "get me every Mac that hasn't synced in a month, sorted oldest first, as CSV" into idiomatic PowerShell, with correct backtick continuations and a pipeline that reads well. It's also good at explaining why Select-Object isn't -Property, and at converting a jq expression you already understand into the object-pipeline equivalent.


What it's bad at is knowing whether a cmdlet exists. Graph's PowerShell surface is enormous, auto-generated, and versioned, which makes it exactly the shape of thing a model will pattern-match its way into. Get-MgDeviceManagementManagedDeviceMacOS is not a cmdlet, but it looks like one, and it will run into a "not recognized" error rather than a wrong answer — which is the good case. The bad case is a real cmdlet with a filter string that silently returns nothing.


So verify before you run. The SDK ships its own answer key:


# Does this cmdlet exist, what URI does it call, what permissions does it need?
Find-MgGraphCommand -Command 'Get-MgDeviceManagementManagedDevice'

# Working backwards from a Graph URI you found in the docs
Find-MgGraphCommand -Uri "/deviceManagement/managedDevices"

# The scopes, expanded
Find-MgGraphCommand -Command Get-MgUser | Select-Object -First 1 -ExpandProperty Permissions

Two habits, and they take seconds: run Find-MgGraphCommand on any cmdlet an AI hands you that you haven't personally used, and run every new query with -Top 5 before you run it with -All.


Ground the model instead of correcting it

A better fix than catching hallucinations is preventing them. Microsoft runs a free, public Learn MCP server at https://learn.microsoft.com/api/mcp — no authentication, streamable HTTP — that lets an MCP-capable assistant search Microsoft's live documentation, fetch full articles, and pull code samples. Point Claude, Copilot, or whatever you're using at it and the cmdlet names stop being guesses.


There's also a Microsoft MCP Server for Enterprise, currently in public preview, that turns natural language into read-only Graph calls against your own tenant. Read its scope before you get excited: it's documented for Entra identity and directory scenarios — users, groups, applications, directory device insights, administrative reporting — not Intune device management. It's the right tool for "which admins don't have MFA," not for "which Macs are noncompliant." That one is still yours to write.


What this looks like at fleet scale

At ten Macs, none of this matters; you'd read the console. At ten thousand, the difference between a console export and a scripted query is whether the answer is reproducible. A saved .ps1 that pulls the same fields with the same filter every Monday gives you a trend line — OS version spread narrowing as an upgrade ring rolls out, stale-sync counts creeping up when a network change breaks check-ins. A CSV somebody clicked out of the admin center gives you a number with no history. Put the scripts in a repo, and give the service principal read-only scopes so a bad afternoon can't turn a reporting script into a remote-wipe script.


The practical takeaway

Install PowerShell 7.6 from Microsoft's .pkg on anything you manage, install Microsoft.Graph.Authentication and Microsoft.Graph.DeviceManagement rather than the whole SDK, and connect with the narrowest scope that answers your question. Let AI write the first draft of every query — that's the fastest path from a bash brain to working PowerShell, and it's not close. Then run Find-MgGraphCommand on anything unfamiliar and -Top 5 on anything new before you turn it loose on the tenant. The failure mode here isn't a script that crashes; it's a query that returns an empty set because the OS string was wrong, and a report that quietly says you have no noncompliant Macs. Group your devices by OperatingSystem once, write the string down, and that whole class of mistake goes away.



Comments


bottom of page