Runbook: Set-DefenderP2License.ps1
Script: scripts/powershell/Set-DefenderP2License.ps1
Owner: Max Simon
Schedule: On-demand (not scheduled — run as needed for new hires or license remediation)
Azure Automation Account: pdr-sharepoint-automation (West US 2, rg-it-automation)
Graduation Path: See CLAUDE.md "Azure Automation Graduation Path" (8 stages)
Compliance: SOC 2 CC6.8 — Anti-malware deployment and coverage
Purpose
Assigns Microsoft Defender for Endpoint P2 licenses to active users who are missing them. Ensures EDR coverage across the org for CIS Control 10 (Malware Defenses) and feeds into Defender deployment validation (Get-DefenderDeploymentStatus.ps1). A user without a P2 license cannot onboard to Defender EDR.
Pipeline
Operator runs Set-DefenderP2License.ps1
|
v
Graph API: GET /v1.0/subscribedSkus (Organization.Read.All)
+ GET /v1.0/users (User.Read.All)
|
v
Identify users missing Defender P2 SKU
|
v
[DryRun?] ── YES ──> CSV preview log + exit
|
NO
|
v
Snapshot: ./logs/snapshots/Set-DefenderP2License_PreState_{timestamp}.json
|
v
Graph API: POST /v1.0/users/{id}/assignLicense (User.ReadWrite.All)
|
v
Results CSV: ./logs/Set-DefenderP2License/DefenderP2_Assignment_{timestamp}.csv
Operational log: ./logs/Set-DefenderP2License/YYYY-MM-DD.log
Prerequisites
- Graph API scopes confirmed:
User.Read.All,Organization.Read.All,Directory.Read.All(already approved in app registration) -
User.ReadWrite.Allscope approved and admin-consented for non-dry-run execution (requires explicit operator sign-off per CLAUDE.md) - Defender P2 licenses purchased and available in tenant (check M365 Admin Center > Billing > Licenses)
- Graph API credentials in Azure Automation Credentials vault (for automation) or
.env(for local dev) - Test account
pdrveriatoexists and is active
Execution Steps
Step 1: Dry Run (Test Account)
.\scripts\powershell\Set-DefenderP2License.ps1 -DryRun
- Targets only
$env:TEST_USER_UPN(pdrveriato) - Uses read-only Graph scopes
- Outputs CSV preview to
./logs/Set-DefenderP2License/ - Review the CSV before proceeding
Step 2: Dry Run (Org-Wide)
.\scripts\powershell\Set-DefenderP2License.ps1 -Scope "All" -ConfirmProduction -DryRun
- Enumerates all active licensed users missing P2
- Still read-only — no changes made
- Review the full list. Verify the count matches expected gap from Get-DefenderDeploymentStatus
Step 3: Live Assignment (Test Account)
.\scripts\powershell\Set-DefenderP2License.ps1
- Assigns P2 to test account only
- Verify in M365 Admin Center that the license appeared on pdrveriato
- Check
./logs/snapshots/for the pre-execution state file
Step 4: Live Assignment (Single Production User)
.\scripts\powershell\Set-DefenderP2License.ps1 -Scope "jane.doe@pacificdebt.com" -ConfirmProduction
- Pick one real user from the dry run list
- Verify license assignment in M365 Admin Center
- Confirm Defender EDR onboarding begins (may take up to 4 hours)
Step 5: Live Assignment (Org-Wide)
.\scripts\powershell\Set-DefenderP2License.ps1 -Scope "All" -ConfirmProduction
- Only run after Steps 1-4 are validated
- Monitor console output for failures
- Review results CSV when complete
Healthy Run
| Indicator | Expected |
|---|---|
| Pre-execution snapshot | Written to ./logs/snapshots/Set-DefenderP2License_PreState_{timestamp}.json |
| Results CSV | Written to ./logs/Set-DefenderP2License/DefenderP2_Assignment_{timestamp}.csv |
| Log file | Written to ./logs/Set-DefenderP2License/YYYY-MM-DD.log |
| Console summary | Shows success/fail counts, no red output |
| Exit code | 0 |
Failure Modes
| Failure | Cause | Resolution |
|---|---|---|
| Graph API 401 | Token expired or client secret rotated | Rotate client secret in Bitwarden, update Azure Automation Credentials vault, verify in .env for local dev |
| Graph API 403 | Missing scope on app registration | Check app registration in Entra portal. Dry-run needs User.Read.All; live assignment needs User.ReadWrite.All with admin consent |
| "Could not auto-detect Defender P2 SKU" | SKU part number not in known list | Check available SKUs in console output. Microsoft may have renamed the SKU. Update $knownDefenderSkus array in script |
| "Need X seats but only Y available" | Insufficient licenses purchased | Purchase additional Defender P2 licenses in M365 Admin Center before re-running |
| "Scope extends beyond test account" error | Missing -ConfirmProduction flag | Add -ConfirmProduction when targeting non-test users. This is a safety guard, not a bug |
| Individual user assignment failure | License conflict, usage location missing, or service plan dependency | Check the error in results CSV. Common fix: set user's Usage Location in Entra before assigning. Resolve per-user, then re-run — already-assigned users are skipped |
| Zero users need assignment | All in-scope users already have P2 | Expected healthy state after full rollout. Script exits cleanly with exit code 0 |
Ongoing Operations
| Cadence | Task |
|---|---|
| After each run | Review results CSV for any ASSIGN_FAILED rows. Remediate individually |
| Weekly | Run Get-DefenderDeploymentStatus.ps1 -Scope All -ConfirmProduction to verify EDR onboarding followed license assignment |
| Monthly | Run dry-run org-wide to check for newly hired users missing P2 |
| Quarterly | Rotate Graph API client secret per credential rotation schedule (CLAUDE.md) |
Sunset Criteria
This script is needed as long as Defender P2 is not auto-assigned via group-based licensing. When met:
- Configure group-based licensing in M365 Admin Center to auto-assign Defender P2 to a dynamic group (e.g., all licensed users)
- Run one final dry-run to confirm zero gaps
- Move script to
completed/and this runbook torunbooks/completed/ - Update PROJECTS.md
Rollback
If the script assigns licenses incorrectly or to the wrong users:
- Stop execution immediately (Ctrl+C if still running)
- Locate the pre-execution snapshot in
./logs/snapshots/Set-DefenderP2License_PreState_{timestamp}.json— this contains the exact license state before changes - Identify affected users from the results CSV (
./logs/Set-DefenderP2License/DefenderP2_Assignment_{timestamp}.csv) — filter forStatus = SUCCESS - Remove the license for each incorrectly assigned user:
```powershell
# For each affected UPN from the results CSV:
Connect-MgGraph -Scopes "User.ReadWrite.All"
$user = Get-MgUser -Filter "userPrincipalName eq 'affected.user@pacificdebt.com'"
Set-MgUserLicense -UserId $user.Id -AddLicenses @() -RemoveLicenses @("SKU_ID_FROM_SNAPSHOT")
```
- Verify removal in M365 Admin Center for each affected user
- Notify the Director of ITOps per incident response procedure (CLAUDE.md)
- Log the incident in
lessons.mdwith timeline, root cause, and preventive changes
Source: secops-pipeline/runbooks/Set-DefenderP2License.md | Last synced: 2026-03-23T08:24:33Z | Do not edit in Zendesk -- changes will be overwritten on next sync.
Comments
0 comments
Please sign in to leave a comment.