Runbook: Get-SystemHealth.ps1
Audience: Jr. Admins, Sysadmins, future ISSO Last Updated: 03/11/2026 Owner: Max S., Director of ITOps Script Version: 3.0.0 Script Path:
scripts/powershell/Get-SystemHealth.ps1
Purpose
Queries device health and user identity status for a target device using Microsoft Graph API. All checks target the -Scope device remotely -- nothing is read from the local machine. This means the script produces accurate results regardless of where it is executed.
This is a READ-ONLY script. It only performs GET requests against the Graph API. No write operations are executed. No changes are made to any system.
What It Checks
| Check | Source | Thresholds |
|---|---|---|
| Storage | Intune managedDevices
|
WARN < 20% free, FAIL < 10% free |
| Memory | Intune managedDevices (physicalMemoryInBytes) |
Reports total only (real-time usage not available via Intune) |
| Last Sync | Intune managedDevices (lastSyncDateTime) |
WARN > 24h, FAIL > 72h |
| Defender RTP |
windowsProtectionState (beta endpoint) |
FAIL if disabled |
| Defender Signatures |
windowsProtectionState (beta endpoint) |
FAIL if > 3 days old |
| Intune Compliance | Intune managedDevices (complianceState) |
FAIL if not compliant |
| Entra ID User | Graph users endpoint |
FAIL if account disabled |
| Defender Alerts | Graph security/alerts_v2
|
WARN if active alerts exist |
Prerequisites
- Local environment set up per environment-setup.md
-
.envfile configured with all required variables (see below) - Azure App Registration has the following Graph API application permissions granted:
- DeviceManagementManagedDevices.Read.All (device inventory + Defender protection state)
- User.Read.All (Entra ID user status)
- SecurityAlert.Read.All (Defender alerts -- requires Defender P2 to return data)
- Admin consent granted for the above permissions in Entra ID
Required Environment Variables
All values come from .env (local) or Azure Automation Credentials vault (production). See .env.example for the template.
| Variable | Description | Where to Find |
|---|---|---|
AZURE_TENANT_ID |
Entra ID tenant ID | Bitwarden > IT Shared > Azure Tenant |
AZURE_CLIENT_ID |
App Registration client ID | Bitwarden > IT Shared > SecOps App Registration |
AZURE_CLIENT_SECRET |
App Registration client secret | Bitwarden > IT Shared > SecOps App Registration |
TEST_DEVICE_NAME |
Default target device name | Pre-filled in .env.example (e.g., PDR-7T93ZW3) |
TEST_USER_UPN |
Default target user UPN | Pre-filled in .env.example (e.g., pdrveriatoservice@pacificdebt.com) |
OPERATOR_NAME |
Your name (for audit logging) | Your name (e.g., Max Simon) |
Optional:
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
INFO |
Minimum log level: DEBUG, INFO, WARN, ERROR, CRITICAL
|
LOG_DIRECTORY |
./logs |
Where log files and JSON exports are written |
GRAPH_API_BASE_URL |
https://graph.microsoft.com/v1.0 |
Override for testing against a different Graph endpoint |
Note: Simulation flags (-SimulateFailure, -SimulateWarning, -SimulateHealthy) do not require Azure credentials -- only TEST_DEVICE_NAME, TEST_USER_UPN, and OPERATOR_NAME are needed.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
-Scope |
String | No | Target device name. Defaults to $env:TEST_DEVICE_NAME. |
-ConfirmProduction |
Switch | Conditional |
Required when -Scope is not the test device. Safety guard to prevent accidental production targeting. |
-SimulateFailure |
Switch | No | Generates a synthetic all-FAIL report. No API calls made. Output prefixed with [SIMULATED]. |
-SimulateWarning |
Switch | No | Generates a synthetic all-WARN report. No API calls made. Output prefixed with [SIMULATED]. |
-SimulateHealthy |
Switch | No | Generates a synthetic all-PASS report. No API calls made. Output prefixed with [SIMULATED]. |
-WhatIf |
Switch | No | Shows what the script would do without making any API calls. |
-Verbose |
Switch | No | Displays detailed log output to console. |
Only one simulation flag may be used at a time. The script will error if multiple are provided.
How to Run
Step 1: Dry Run (always do this first)
.\scripts\powershell\Get-SystemHealth.ps1 -WhatIf -Verbose
Expected output: A log message stating what queries would be performed. No API calls are made.
Step 2: Run Against Test Device
.\scripts\powershell\Get-SystemHealth.ps1 -Verbose
This runs against the default test device ($env:TEST_DEVICE_NAME). No -ConfirmProduction flag needed.
Step 3: Run Against a Production Device
.\scripts\powershell\Get-SystemHealth.ps1 -Scope "PROD-PC-001" -ConfirmProduction -Verbose
The -ConfirmProduction flag is required for any device that is not the test device. Without it, the script will block execution, log the attempt, and exit.
Simulation Mode (Pipeline Certification)
# Test that the script handles all-FAIL scenarios
.\scripts\powershell\Get-SystemHealth.ps1 -SimulateFailure
# Test that the script handles degraded scenarios
.\scripts\powershell\Get-SystemHealth.ps1 -SimulateWarning
# Test that the script handles clean scenarios
.\scripts\powershell\Get-SystemHealth.ps1 -SimulateHealthy
Simulation mode uses synthetic data. No Azure credentials or API calls are needed.
Expected Output
Console
The script prints a formatted health report to the console:
========================================
SYSTEM HEALTH REPORT
Device: PDR-7T93ZW3
Model: Dell Inc. Latitude 3540
OS: Windows 10.0.26200.7840
User: pdrveriatoservice@pacificdebt.com
Source: Graph API (all checks target PDR-7T93ZW3)
2026-03-11 19:02:45
========================================
--- Device Health ---
Storage 67.9% free - 152.81 GB of 225.18 GB [PASS]
Memory Memory data not reported by Intune [WARN]
Last Sync 8.8h ago [PASS]
Defender RTP Enabled [PASS]
Defender Sigs Age: 0d - Engine: 1.1.26010.1 [PASS]
--- Compliance & Identity ---
Intune Compliance compliant [PASS]
Entra ID User PDR Veriatio Service - Enabled [PASS]
Defender Alerts Defender alerts unavailable: ... [WARN]
----------------------------------------
OVERALL: [WARN]
Results: ./logs/health/SystemHealth_PDR-7T93ZW3_20260311_190245.json
JSON Export
Every run exports a JSON file to ./logs/health/:
-
Live runs:
SystemHealth_{DeviceName}_{timestamp}.json -
Simulated runs:
SystemHealth_{DeviceName}_SIM-{Mode}_{timestamp}.json
JSON structure:
{
"Metadata": { "ScriptName", "Version", "Device", "User", "Operator", "Timestamp", "Environment", "SimulationMode", "DataSource" },
"Device": { "DeviceName", "Model", "Manufacturer", "OS", "OSVersion" },
"DeviceHealth": {
"Storage": { "TotalGB", "FreeGB", "FreePercent", "Status" },
"Memory": { "TotalGB", "Status" },
"LastSync": { "LastSync", "HoursSinceSync", "Status" },
"Defender": { "RealTimeProtection", "RealTimeProtectionStatus", "SignatureAgeDays", "SignatureVersion", "EngineVersion", "SignatureStatus", "Status" }
},
"ComplianceAndIdentity": {
"IntuneCompliance": { "ComplianceState", "Status" },
"EntraUser": { "DisplayName", "UPN", "AccountEnabled", "CreatedDate", "Status" },
"DefenderAlerts": { "ActiveAlertCount", "Alerts", "Status" }
},
"Summary": { "OverallStatus", "PassCount", "WarnCount", "FailCount" }
}
Log File
All actions are logged to ./logs/Get-SystemHealth.log using the project standard format:
[2026-03-11T19:02:42Z] [INFO] [Get-SystemHealth] [Max Simon] [PDR-7T93ZW3] [Start] --Beginning health check for device: PDR-7T93ZW3
Known Limitations
1. Memory (physicalMemoryInBytes) returns 0 on some devices
Symptom: Memory check shows [WARN] Memory data not reported by Intune instead of total GB.
Cause: Some devices do not report physicalMemoryInBytes to Intune. This is a known Intune inventory gap, not a script bug. The field returns 0 or null.
Impact: Memory check will WARN on affected devices. All other checks are unaffected.
Workaround: None -- this is an Intune-side limitation. If this is widespread across the fleet, consider suppressing the WARN to avoid alert fatigue, or monitor whether Intune updates improve reporting.
2. Defender Alerts returns 400 until Defender P2 is active
Symptom: Defender Alerts check shows [WARN] Graph API returned 400 : Bad Request.
Cause: The security/alerts_v2 endpoint requires Defender for Endpoint Plan 2, which is not yet deployed (target: late March / early April 2026).
Impact: Defender Alerts will always WARN until P2 is active. Overall status will be WARN at best.
Resolution: Once Defender P2 is deployed and the SecurityAlert.Read.All permission is functional, this check will return real alert data. No script changes needed.
3. CPU load is not available
Intune does not expose processor name or real-time CPU utilization via Graph API. This data was removed in v3.0.0 (previously read from the local machine, which was inaccurate for remote targets).
4. Memory reports total only, not usage
Intune reports physicalMemoryInBytes (total installed RAM) but does not expose real-time memory usage. The memory check always returns PASS when data is available, since there is no usage percentage to threshold against.
5. Windows Update per-hotfix data not available
Intune does not expose individual hotfix history via Graph API. Windows Update status is covered by the Intune compliance check, which includes update compliance in its evaluation.
Graduation Status
| Stage | Status | Date | Notes |
|---|---|---|---|
| 1. Local Build & Test | Complete | 2026-03-11 | v3.0.0 validated live against PDR-7T93ZW3 |
| 2. Production Validation (Manual) | Not started | -- | Run against a real production device with -ConfirmProduction
|
| 3. Service Account Creation | Not started | -- | svc-systemhealth@pacificdebt.com |
| 4. Credential Storage | Not started | -- | Azure Automation Credentials vault |
| 5. Runbook Creation & Testing | Not started | -- |
pdr-sharepoint-automation (West US 2) |
| 6. Schedule & Monitor | Not started | -- | Recommended: daily at 06:00 UTC |
| 7. Parallel Run (7 days) | Not started | -- | |
| 8. Decommission Local | Not started | -- |
Rollback Procedure
This is a read-only script. It does not modify any system.
There is nothing to roll back. The script only:
- Authenticates to Graph API (reads a token)
- Performs GET requests against Intune, Entra ID, and Defender
- Writes a JSON file to the local
./logs/health/directory - Writes log entries to
./logs/Get-SystemHealth.log
If the script produces incorrect output:
- Check the JSON export for the specific run -- the data came from Graph API and reflects what Intune/Entra reported at query time
- Compare against the Intune portal (
https://intune.microsoft.com> Devices) to verify the data matches - If there is a discrepancy between script output and portal, log it in
lessons.mdand investigate whether the Graph API response differs from portal data
If the script fails mid-execution:
- Check
./logs/Get-SystemHealth.logfor the last logged action - The partial JSON export may not exist (it is written at the end). No cleanup is needed.
- Re-run the script. There are no side effects from partial runs.
Escalation Path
| Situation | Action |
|---|---|
| Script fails to authenticate (401/403) | Verify AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET in .env. Check if the client secret has expired (90-day rotation). Check Bitwarden for current values. |
| Device not found in Intune | Verify the device name matches exactly (case-sensitive). Check that the device is enrolled in Intune via the portal. |
| All checks return ERROR | Graph API may be down or throttled. Check Microsoft 365 Service Health. Retry after 5 minutes. |
| Unexpected data (e.g., wrong device info) | If Intune returns data for a different device than expected, there may be a duplicate device name. Check Intune portal and escalate to Director of ITOps. |
| Script parse errors or crashes | Check if the script was modified locally. Run git diff scripts/powershell/Get-SystemHealth.ps1 to see changes. Reset with git checkout scripts/powershell/Get-SystemHealth.ps1 if needed. |
| Anything not covered above | Escalate to Max S., Director of ITOps. Include: what you ran, what happened, and the contents of ./logs/Get-SystemHealth.log. |
Source: secops-pipeline/runbooks/Get-SystemHealth.md | Last synced: 2026-03-23T08:24:31Z | Do not edit in Zendesk -- changes will be overwritten on next sync.
Comments
0 comments
Please sign in to leave a comment.