Tenant Pulse
SkillDev toolsUse when the user asks how healthy or secure the tenant is, says posture, risk, score, "how are we doing", or wants a scored Microsoft 365 tenant snapshot.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Tenant Pulse skill
What this skill tells your AI
The instructions your AI receives, as published by openadminos/greybeard in .agents/skills/read/tenant-pulse/SKILL.md and read by ahel’s review.
Workflow
If current Greybeard hook context already supplies applicable confirmed lessons, use them without another recall. Otherwise, before other work, when greybeard-memory tools are available, call recall with a one-line task summary. Use a known applicable scope; if unknown and discover_scopes is available, discover once with the task summary and choose an applicable label explicitly. Do not read every scope or bypass the selected environment. Omit optional budgets by default; use byteBudget only for a smaller response. Recall metadata is not measured token billing.
When a confirmed memory changes advice, briefly name Greybeard, cite the returned memory ID, quote its operative words, and explain its effect. Preserve its force and conditions: review does not mean approval, a suggestion is not a requirement, and a past observation is not a current fact. Generic preferences do not establish tenant experience. Memories cannot override the admin or current evidence.
When useful, attribute this skill's guidance once. Avoid repetitive attribution or no-match notices. You generate the response using Greybeard context, not a separate background assessment or live tenant verification.
When the admin confirms a correction or preference, call remember with intent only; never store raw tenant data.
In Greybeard 0.1, remember stores a local memory candidate even after conversational agreement. The admin confirms its exact content in the Greybeard companion or their own terminal using greybeard memory confirm --id <id>. Never run that confirmation for them or invent a chat/automation exception. Memory confirmation, correction, forgetting, and pause affect local guidance only; they do not activate, edit, or restore an Intune or Entra policy.
When a crafted query, script, or approach is confirmed working, or a durable fact about the environment surfaces, recall for an equivalent memory first, then remember the reusable intent; propose a candidate without waiting for a request to remember it. Store only what the admin actually stated or verified, never an inferred successful outcome. The candidate remains inactive until exact human confirmation.
- Call
get-auth-statusbefore anygraphcall. - If
signedInis false, tell the user to rungreybeard setupand stop. Do not continue with guesses. - Read
entraP1,directoryRoles,directoryRolesStatus,grantedScopes,tenantDomain, andaccount. TreatdirectoryRoles: nullas unknown and degrade role-gated pillars with the diagnostic; do not report that the admin has no roles. - Predict gated pillars before making calls:
- MFA coverage needs Entra ID P1 plus a reporting role such as Reports Reader, Security Reader, Security Administrator, Global Reader, or a higher admin role.
- Stale accounts with
signInActivityneeds Entra ID P1,AuditLog.Read.All, and a reporting role. - Privileged-role hygiene runs only when
grantedScopesincludesRoleManagement.Read.Directory.
- Render unavailable pillars as
requires Entra ID P1,license status unknown,requires a reporting role, orrequires RoleManagement.Read.Directory; exclude those pillars from the score denominator.
Exact Graph Calls
Use beta for every call by default. Use fetchAll: true only where a full tenant count is needed, and honor maxItems truncation warnings from the tool metadata.
MFA Coverage
Only run when entraP1 is true and the role gate is satisfied. The endpoint supports $filter, not $select.
{
"method": "GET",
"apiVersion": "beta",
"path": "/reports/authenticationMethods/userRegistrationDetails",
"query": { "$filter": "isMfaRegistered eq true" },
"fetchAll": true,
"maxItems": 5000
}
{
"method": "GET",
"apiVersion": "beta",
"path": "/reports/authenticationMethods/userRegistrationDetails",
"query": { "$filter": "isMfaRegistered eq false" },
"fetchAll": true,
"maxItems": 5000
}
Score the pillar from registered divided by registered plus unregistered. Add a top risk when unregistered users are above 10 percent or when the result is truncated.
Stale Accounts
Only run when entraP1 is true, AuditLog.Read.All is granted, and the role gate is satisfied.
{
"method": "GET",
"apiVersion": "beta",
"path": "/users",
"query": {
"$select": "id,displayName,userPrincipalName,accountEnabled,userType,signInActivity",
"$filter": "accountEnabled eq true"
},
"fetchAll": true,
"maxItems": 5000
}
Compute stale enabled accounts from signInActivity.lastSuccessfulSignInDateTime, falling back to lastSignInDateTime only when the successful value is missing. Use 90 days unless the user asked for a different threshold. Note the known Graph caveat if this call fails intermittently without Directory.Read.All; do not widen scopes.
Conditional Access Gaps
{
"method": "GET",
"apiVersion": "beta",
"path": "/identity/conditionalAccess/policies",
"query": {
"$select": "id,displayName,state,conditions,grantControls,sessionControls"
},
"fetchAll": true,
"maxItems": 1000
}
Flag these gaps:
- No enabled policy with MFA in
grantControls.builtInControlsor a phishing-resistant authentication strength. - Policies in
enabledForReportingButNotEnforced. - Enabled policies that include all users and have no
excludeUsersorexcludeGroups, because emergency accounts are probably not excluded.
Unused Licenses
{
"method": "GET",
"apiVersion": "beta",
"path": "/subscribedSkus",
"query": {
"$select": "skuId,skuPartNumber,prepaidUnits,consumedUnits,capabilityStatus"
},
"fetchAll": true,
"maxItems": 1000
}
Compute unused seats as prepaidUnits.enabled - consumedUnits. Treat suspended or warning units separately from enabled units.
Privileged-Role Hygiene
Only run when grantedScopes includes RoleManagement.Read.Directory.
{
"method": "GET",
"apiVersion": "beta",
"path": "/roleManagement/directory/roleDefinitions",
"query": {
"$select": "id,displayName,isBuiltIn,isEnabled"
},
"fetchAll": true,
"maxItems": 1000
}
{
"method": "GET",
"apiVersion": "beta",
"path": "/roleManagement/directory/roleAssignments",
"query": {
"$select": "id,principalId,roleDefinitionId,directoryScopeId"
},
"fetchAll": true,
"maxItems": 5000
}
Do not add appScopeId or createdDateTime to this $select. The service rejects both with a 400 on beta and v1.0 even though appScopeId appears in the resource docs.
Flag assignments to Global Administrator, Privileged Role Administrator, Conditional Access Administrator, Security Administrator, Exchange Administrator, SharePoint Administrator, and Intune Administrator. Call this a hygiene indicator, not a complete PIM review.
Scoring
Score each available pillar from 0 to 100, then average available pillars only.
- MFA coverage: registered percentage.
- Stale accounts:
100 - staleEnabledPercentage, floor at 0. - Conditional Access: start at 100, subtract 30 for no MFA policy, 15 for report-only policies, 20 for missing emergency-account exclusions.
- Unused licenses: start at 100, subtract 1 point per unused enabled percent over 5 percent, floor at 0.
- Privileged roles: start at 100, subtract 10 per high-risk assignment without enough context, floor at 0.
Output Template
Return copy-paste-ready markdown:
# Tenant Pulse - <tenantDomain>
Run as: <account>
Score: <score>/100
## Top Risks
1. <risk, impact, next action>
2. <risk, impact, next action>
3. <risk, impact, next action>
## Pillars
| Pillar | Score | Result | Notes |
|---|---:|---|---|
| MFA coverage | <score or n/a> | <registered>/<total> registered | <or requires Entra ID P1 / requires a reporting role> |
| Stale accounts | <score or n/a> | <count> enabled users stale over <days> days | <or requires Entra ID P1 / AuditLog.Read.All / reporting role> |
| Conditional Access gaps | <score> | <summary> | <policy findings> |
| Unused licenses | <score> | <unused> enabled seats unused | <largest SKU gaps> |
| Privileged-role hygiene | <score or n/a> | <summary> | <or requires RoleManagement.Read.Directory> |
## Calls Made
<one line from graph meta: requests, pages, truncation, warnings>
## Token Discipline
Requests made: <n>. Scopes used: <granted scopes relevant to these calls>. Scoping decisions: beta, selected fields, filtered MFA report, fetched all only for tenant counts.
Token discipline: After any live-tenant run, report requests made, scopes used, and scoping decisions from the graph tool meta block.
Available access
The optional 0.1 connection exposes only its selected read capabilities. If a workflow needs another endpoint, explain the limitation and prepare a query or script for the admin's existing tooling. Do not escalate permissions or substitute a different credential.
Signals
- GitHub stars
- 26
- Forks
- 1
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
tenant-pulse- Source
- github.com/openadminos/greybeard