Skip to content
Back to skills

Tenant Pulse

ASecurity

Use 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.

  • 18 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 19, 2026
ai-agentsapisecurity

Works with

  • terminal
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned September 19, 2026

npx -y skills add OpenAdminOS/greybeard --skill tenant-pulse --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tenant Pulse?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tenant Pulse
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/openadminos-tenant-pulse/badge)](https://www.skillsdirectory.com/skills/openadminos-tenant-pulse)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: tenant-pulse
description: Use 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.
---

# Tenant Pulse

## 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.

1. Call `get-auth-status` before any `graph` call.
2. If `signedIn` is false, tell the user to run `greybeard setup` and stop. Do not continue with guesses.
3. Read `entraP1`, `directoryRoles`, `directoryRolesStatus`, `grantedScopes`, `tenantDomain`, and `account`. Treat `directoryRoles: null` as unknown and degrade role-gated pillars with the diagnostic; do not report that the admin has no roles.
4. 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 `signInActivity` needs Entra ID P1, `AuditLog.Read.All`, and a reporting role.
   - Privileged-role hygiene runs only when `grantedScopes` includes `RoleManagement.Read.Directory`.
5. Render unavailable pillars as `requires Entra ID P1`, `license status unknown`, `requires a reporting role`, or `requires 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`.

```json
{
  "method": "GET",
  "apiVersion": "beta",
  "path": "/reports/authenticationMethods/userRegistrationDetails",
  "query": { "$filter": "isMfaRegistered eq true" },
  "fetchAll": true,
  "maxItems": 5000
}
```

```json
{
  "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.

```json
{
  "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

```json
{
  "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.builtInControls` or a phishing-resistant authentication strength.
- Policies in `enabledForReportingButNotEnforced`.
- Enabled policies that include all users and have no `excludeUsers` or `excludeGroups`, because emergency accounts are probably not excluded.

### Unused Licenses

```json
{
  "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`.

```json
{
  "method": "GET",
  "apiVersion": "beta",
  "path": "/roleManagement/directory/roleDefinitions",
  "query": {
    "$select": "id,displayName,isBuiltIn,isEnabled"
  },
  "fetchAll": true,
  "maxItems": 1000
}
```

```json
{
  "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:

```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.

Files in this skill

  • SKILL.md8.9 KB
  • agents/openai.yaml201 B
  • test.md449 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.