Microsoft 365 Security Posture
Overview
Security checks are among the most high-value tasks an MSP can perform in a customer's M365 tenant. Account compromises, inadequate MFA coverage, and misconfigured mail rules are the leading causes of M365 security incidents. This skill covers the key checks and indicators that separate a secure tenant from a vulnerable one.
Anti-triggers
"Security" is the most overloaded word in the MSP stack. This skill is identity posture inside one M365 tenant, read via Graph:
- A detection or alert on an endpoint — malware, ransomware,
suspicious process, isolate-the-host. That is EDR/MDR, not Entra; use
huntress,sentinelone,blackpoint, orblumira. - Enforcing a baseline rather than reporting on it — this skill
tells you conditional access is missing; deploying and holding the
standard across tenants is
cipp(cipp-standards) orinforcer(inforcer-baseline-alignment). - The same posture check across every tenant — use
cipp(cipp-security). One tenant at a time is what this skill does. - Phishing, quarantine, and mail-flow threat verdicts — those
belong to the mail-security gateway:
mimecast,abnormal,ironscales, or theemail-securitypack. - Evidence for an audit or insurance questionnaire — a control
attestation is not a Graph query; use the
compliance-pack.
MFA Status Audit
Check All Users' Authentication Methods
MFA enrollment lives on the authentication/methods endpoint per user. Users with only a passwordAuthenticationMethod entry have no MFA.
GET /v1.0/users/{userId}/authentication/methods
Response — user WITH MFA:
{
"value": [
{
"@odata.type": "#microsoft.graph.microsoftAuthenticatorAuthenticationMethod",
"id": "aad-method-id",
"displayName": "iPhone",
"createdDateTime": "2023-06-01T10:00:00Z"
},
{
"@odata.type": "#microsoft.graph.passwordAuthenticationMethod"
}
]
}
Response — user WITHOUT MFA (vulnerable):
{
"value": [
{
"@odata.type": "#microsoft.graph.passwordAuthenticationMethod"
}
]
}
Bulk MFA Report
Use Microsoft Graph Reports for tenant-wide MFA status:
GET /v1.0/reports/authenticationMethods/userRegistrationDetails
Response per user:
{
"id": "user-guid",
"userPrincipalName": "jsmith@contoso.com",
"isMfaRegistered": true,
"isMfaCapable": true,
"isSsprRegistered": false,
"methodsRegistered": ["microsoftAuthenticator", "softwareOath"]
}
This is the fastest path to a full tenant MFA audit.
MFA Method Risk Levels
| Method | Security Level | Notes |
|---|---|---|
| FIDO2 hardware key | Highest | Phishing-resistant |
| Windows Hello for Business | Highest | Device-bound |
| Microsoft Authenticator (passwordless) | High | Number matching recommended |
| OATH hardware token | High | |
| Microsoft Authenticator (OTP) | Medium | Better than SMS |
| Software OATH (other app) | Medium | |
| SMS/Phone | Low | Susceptible to SIM swap |
| Password only | None | Unacceptable for business |
Sign-In Risk and Risky Users
Get Risky Users (Entra ID P2 Required)
GET /v1.0/identityProtection/riskyUsers?$filter=riskState eq 'atRisk'&$select=id,userPrincipalName,riskLevel,riskState,riskLastUpdatedDateTime
Risk Levels: low, medium, high
Get Risky Sign-Ins
GET /v1.0/auditLogs/signIns?$filter=riskLevelDuringSignIn ne 'none'&$select=userPrincipalName,riskLevelDuringSignIn,location,createdDateTime&$top=50
Dismiss Risk for a User (After Remediation)
POST /v1.0/identityProtection/riskyUsers/dismiss
Content-Type: application/json
{
"userIds": ["user-guid"]
}
Get Recent Sign-In Logs
GET /v1.0/auditLogs/signIns?$filter=userPrincipalName eq 'user@contoso.com'&$select=createdDateTime,userPrincipalName,ipAddress,location,status,clientAppUsed,riskLevelDuringSignIn&$top=20&$orderby=createdDateTime desc
Key fields for incident response:
status.errorCode: 0= success, nonzero = failureipAddressandlocationfor geolocation anomaliesclientAppUsed— legacy auth clients are high riskconditionalAccessStatus: notApplied= CA policy gap
Suspicious Inbox Rules
Attackers often create hidden inbox rules to forward mail or hide replies. Check for:
GET /v1.0/users/{userId}/mailFolders/inbox/messageRules
Red flags:
- Rules forwarding to external addresses
- Rules deleting messages matching keywords ("invoice", "password", "wire transfer")
- Rules moving messages to obscure folders
- Rules created at unusual times
{
"displayName": "hidden rule",
"conditions": { "subjectContains": ["invoice"] },
"actions": { "forwardTo": [{ "emailAddress": { "address": "attacker@external.com" } }] },
"isEnabled": true
}
Legacy Authentication
Legacy auth protocols (IMAP, POP3, SMTP AUTH, basic auth) bypass MFA. Identify users still using them:
GET /v1.0/auditLogs/signIns?$filter=clientAppUsed eq 'IMAP' or clientAppUsed eq 'POP3' or clientAppUsed eq 'Exchange ActiveSync'&$select=userPrincipalName,clientAppUsed,createdDateTime&$top=100
Recommended: Block legacy auth via Conditional Access policy.
Conditional Access Overview
Check if CA policies are configured:
GET /v1.0/identity/conditionalAccess/policies?$select=id,displayName,state,conditions,grantControls
state values: enabled, disabled, enabledForReportingButNotEnforced (report-only)
MSP baseline CA policies to verify exist:
- Require MFA for all users
- Block legacy authentication protocols
- Require compliant device for admin roles
- Block sign-in from high-risk countries (optional)
Security Score
Get the tenant's Microsoft Secure Score:
GET /v1.0/security/secureScores?$top=1
Response:
{
"value": [{
"currentScore": 52.4,
"maxScore": 120.0,
"averageComparativeScores": [
{ "basis": "AllTenants", "averageScore": 38.2 }
]
}]
}
Score improvement recommendations:
GET /v1.0/security/secureScoreControlProfiles?$select=title,maxScore,implementationStatus,controlCategory
Account Compromise Indicators
When a user reports suspicious activity, check these in order:
| Check | Command | Red Flag |
|---|---|---|
| Recent sign-ins | GET /auditLogs/signIns |
Unfamiliar IP, country, time |
| MFA changes | GET /auditLogs/directoryAudits |
MFA method added/removed |
| Inbox rules | GET /mailFolders/inbox/messageRules |
External forwarding |
| Sent items | GET /messages from Sent folder |
Phishing sent from account |
| OAuth apps | GET /oauth2PermissionGrants |
Unknown app granted access |
Full Compromise Response Checklist
- Revoke sessions:
POST /v1.0/users/{id}/revokeSignInSessions - Reset password via admin center (forces re-auth)
- Remove suspicious inbox rules
- Review mail sent in the compromised window
- Check OAuth app consents and revoke suspicious ones
- Notify user and require MFA re-enrollment
- File incident in PSA with timeline
Permissions Required
| Task | Microsoft Graph Permission |
|---|---|
| MFA registration report | UserAuthenticationMethod.Read.All |
| Authentication methods | UserAuthenticationMethod.Read.All |
| Sign-in logs | AuditLog.Read.All |
| Risky users | IdentityRiskyUser.Read.All (P2) |
| Conditional access | Policy.Read.All |
| Security score | SecurityEvents.Read.All |
| Revoke sessions | Directory.ReadWrite.All |
Related Skills
- M365 Users - Disable account, revoke sessions
- M365 Mailboxes - Inbox rule details, send-on-behalf audit
- M365 API Patterns - Auth, audit log query patterns