entraadm MCP

io.github.shigechika由 GitHub 账号 shigechika 发布v1.10.1更新于 Nov 3, 2026

MCP server for Microsoft Entra ID sign-in and audit-log triage (read-only by default)

概览

AI 生成的概览

只读的 Microsoft Entra ID 登录与审核日志排查 MCP 服务器,兼查 Azure 虚拟机备份和 Microsoft 365 服务健康状况。

可以做什么
  • 查看账户与认证:账户启用状态、已注册的 MFA 方式,以及带 AADSTS 错误码解释的单用户登录事件。
  • 汇总全租户登录活动:失败统计与密码喷洒嫌疑、按来源 IP 的成功统计、单一 IP 的全部登录,以及疑似被盗账户。
  • 查看目录变更记录和 Microsoft 365 服务健康状况,或一次调用获取合并健康、失败、管理员操作与嫌疑账户的每日简报。
  • 检查 Azure 治理信息:可见订阅、虚拟机备份状态与策略,以及虚拟机活动与重启原因。
  • 发起 Microsoft Purview 审核日志搜索,并读取其状态与汇总记录。
  • 可选地将某个账户在所有位置登出,仅在显式启用时提供。
适用场景
适合服务台或安全团队排查 Entra ID 登录问题、调查密码喷洒与账户接管,以及同时覆盖 Azure 虚拟机备份和 Microsoft 365 事件的每日巡检。也适合无人值守、仅应用身份运行、希望使用固定只读工具集而非通用 Graph 查询工具的部署。
运行要求
本地 Python 包(uv tool install 或 pip install entraadm-mcp),通过 stdio 运行。要么设置 ENTRAADM_TENANT_ID、ENTRAADM_CLIENT_ID 和 ENTRAADM_CLIENT_SECRET 使用应用身份,要么三者都不设并依赖已有的 az login 会话。各工具需要 User.Read.All、AuditLog.Read.All、UserAuthenticationMethod.Read.All、ServiceHealth.Read.All 等 Graph 应用权限;Azure 工具需要每个订阅上的 Reader 角色。可选配置包括 ENTRAADM_CONFIG(多租户)、ENTRAADM_SITES_INI、ENTRAADM_HOME_COUNTRIES 和 ENTRAADM_DEADLINE。
安装前请注意
需要客户端密钥(ENTRAADM_CLIENT_SECRET)或具有广泛租户读取权限的 az login 会话,包括登录日志和审核日志。设置 ENTRAADM_ENABLE_REVOKE_SESSIONS=1 会注册 revoke_sign_in_sessions,使某账户的刷新令牌和会话 Cookie 失效;在共享或未经审批的部署中应保持关闭,并仅在需要处授予 User.RevokeSessions.All。审核日志搜索会占用租户搜索配额,可能返回 rate_limited。登录与审核数据会发送给 MCP 客户端和模型。

安装

在 SourceWeft 中

  1. 打开 控制台中的 entraadm MCP,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

entraadm-mcp

English | 日本語

MCP server for Microsoft Entra ID sign-in and audit-log triage, plus the two Azure-side questions a morning patrol asks with the same credential: are our Azure VMs backed up, and is Microsoft 365 itself degraded right now. Read-only, except for one opt-in tool that signs a compromised account out everywhere.

Why this instead of the official Microsoft MCP Server for Enterprise

Microsoft ships an official MCP Server for Enterprise for Entra ID data. It is a good fit for an interactive admin at a keyboard, and is not a fit for an unattended triage bot:

  • Delegated auth only. The official server does not support app-only (client credentials) auth, so it cannot run headless behind a service account. entraadm-mcp is built for that case: app-only in production, with a delegated (az login) fallback for local development.
  • A general-purpose Graph query tool, not a fixed tool set. The official server exposes one tool that lets the model construct arbitrary GET/schema-discovery calls against Microsoft Graph. That is flexible for a human, and awkward to put behind an allow-list for an automated triage profile. entraadm-mcp exposes seventeen fixed, read-only tools instead (plus one opt-in write, below).
  • No AADSTS translation. Sign-in failures come back as raw error codes; triage still needs a lookup table. entraadm-mcp annotates every sign-in failure with what the code actually means.
  • No cross-request aggregation. Microsoft Graph itself cannot filter sign-ins on status/errorCode server-side, and has no built-in password-spray view. signin_failure_stats aggregates client-side and flags IPs with failed sign-ins against many distinct users — the pattern Entra's per-account smart lockout does not catch on its own.

Tools

ToolWhat it answers
health_checkIs Graph reachable, and can this credential read sign-in logs?
get_userIs this account enabled, synced from on-prem, and what are its licenses?
signin_logsWhy did this user's sign-in fail (or succeed), with the AADSTS code translated?
signin_failure_statsTenant-wide failure aggregation: top error codes, users, apps, source IPs, and password-spray suspects
signin_success_statsTenant-wide success aggregation by source IP: IPs shared by several accounts, legacy-auth (SMTP/IMAP) successes — the view that finds a breach
compromise_suspectsWhich accounts were probably taken over: an outside IP that sprayed many accounts and got into some (spray_breaches), successes from outside the home countries (foreign_successes, high when over a legacy protocol, through Azure CLI / PowerShell, or from 3+ countries within 24 hours), and Microsoft's own risky users when the tenant has Entra ID P2
signin_by_ipEvery sign-in from one source IP: who got in from it, who was tried, and when
directory_auditsWho changed what in the directory (block/unblock, attribute edits), and when?
get_user_auth_methodsIs MFA actually registered for this account?
list_subscriptionsWhich Azure subscriptions can this credential see? (ARM)
vm_backup_statusWhich Azure VMs are backed up by Azure Backup, did the last backup succeed, and is any recovery point stale? (ARM)
vm_backup_policiesWhat schedule and retention do the Azure VM backup policies have? (ARM)
vm_activityDid someone restart this Azure VM, or did Azure (host failure, maintenance)? Activity log and Resource Health for a time window (ARM)
m365_service_healthIs a Microsoft 365 service degraded, and which incidents and advisories are open?
audit_log_search_startWhat did this account (or this IP) do in OneDrive/SharePoint, Exchange and Entra? Submits a Microsoft Purview audit log search, which runs for some minutes in Microsoft 365
audit_log_search_resultThat search's status, then its records: counts by operation, service, record type, user and client IP (with first/last seen) and a trimmed record list
daily_briefOne-call summary combining m365_service_health, signin_failure_stats, directory_audits and compromise_suspects (counts and top 5)

Every tool above is read-only. audit_log_search_start creates a saved audit search object (the one audit_log_search_result then reads) and changes no directory, mailbox or file data. One state-changing tool exists and is off by default:

ToolWhat it does
revoke_sign_in_sessionsSigns one account out everywhere (Graph revokeSignInSessions: refresh tokens and session cookies are invalidated). For a confirmed compromise. Registered only when ENTRAADM_ENABLE_REVOKE_SESSIONS=1

It looks the account up by exact userPrincipalName first and refuses a guest. Every result has the same keys, including revoked and not_covered: it does not disable the account, reset the password or remove MFA methods, and access tokens already issued keep working until they expire (typically about an hour). revoked is true when Graph confirmed it, false when nothing was done (a refusal, a deadline before the request was sent, a 4xx from Graph), and null when the outcome is unknown (a response lost to a network error, a 5xx after the retries, a deadline after the request was sent, an unreadable success answer); calling again is then safe, revocation is idempotent. Leave the switch unset on any deployment that is not behind its own approval step (a shared gateway, for instance) -- the tool is then absent from tools/list. Unblocking an account and resetting a password stay out of scope.

The four Azure Resource Manager tools live in an Entra-named server on purpose: they use the same app registration and the same azure-identity credential object (a second token scope, https://management.azure.com/.default), run in the same deployment and are covered by the same health_check. A separate server for these tools would double the operational surface. They are governance reads, not resource management; a subscription the credential has no Reader role on simply does not appear in list_subscriptions.

Auth model

Two auth modes, selected by which environment variables are set:

ModeWhenEnv vars
app-onlyAll three setENTRAADM_TENANT_ID, ENTRAADM_CLIENT_ID, ENTRAADM_CLIENT_SECRET
azure-cliNone set(uses the current az login session)

Setting one or two of the three app-only variables is a configuration error and the server refuses to start, rather than silently falling back to a different auth mode than intended.

Required Graph permissions

Tool(s)PermissionNotes
get_user (base fields)User.Read.All
signin_logs, signin_failure_stats, signin_success_stats, compromise_suspects, signin_by_ip, directory_audits, get_user's sign_in_activity fieldAuditLog.Read.All (app-only) or the Reports Reader directory role (delegated)
get_user_auth_methodsUserAuthenticationMethod.Read.AllApp-only only; not available under delegated (az login) auth in a typical tenant role assignment
m365_service_health, daily_brief's service_health sectionServiceHealth.Read.AllUnder delegated auth the signed-in user also needs an Entra role that may read service health (for example Service Support Administrator or Global Reader)
audit_log_search_start, audit_log_search_resultAuditLogsQuery.Read.All, or the service-scoped AuditLogsQuery-OneDrive.Read.All / AuditLogsQuery-SharePoint.Read.All / AuditLogsQuery-Exchange.Read.All / AuditLogsQuery-Entra.Read.All (application permissions)Optional. With only a service-scoped permission, pass the matching service. Each search counts against the tenant's audit search limits (searches per day, searches running at once); a 429 comes back as rate_limited: true and is not retried
compromise_suspects' microsoft_risky_users, the identity_protection probe (health_check, daily_brief)IdentityRiskyUser.Read.AllOptional. Identity Protection needs Entra ID P2: without it Graph answers 403 "not licensed" and the probe reports not_licensed; nothing else changes
revoke_sign_in_sessions (opt-in)User.RevokeSessions.All (app-only) or the User Administrator / Helpdesk Administrator directory role (delegated; Privileged Authentication Administrator for an administrator account)Only when ENTRAADM_ENABLE_REVOKE_SESSIONS=1. A write permission: grant it only to a deployment that needs the tool. The account lookup before it needs User.Read.All. Without the permission the tool answers missing_permission for the auth mode in use and nothing changes

Required Azure RBAC

Tool(s)RoleNotes
list_subscriptionsnoneLists whatever the credential can see; an empty list means no role has been granted anywhere yet
vm_backup_status, vm_backup_policies, vm_activityReader on each subscriptionReader is */read and covers every endpoint used. Backup Reader alone is not enough (it has no Microsoft.Compute/virtualMachines/read). Granted to the service principal (app-only) or to the signed-in user (azure-cli)

Azure RBAC is optional: health_check reports ARM reachability and the number of visible subscriptions as an informational arm probe that never changes status, so a deployment used only for Entra triage stays healthy.

A missing permission never crashes a tool. It degrades that tool (or that one field) to {"error": "...", "missing_permission": "..."} with a human-readable explanation of what role or permission is needed, so health_check and every other tool stay usable even before full permissions are granted.

Identity Protection (Entra ID P2). compromise_suspects adds Microsoft's own risk verdicts when the tenant has them: a cheap riskyUsers?$top=1 probe classifies each tenant as licensed, not_licensed (403 "not licensed": no Entra ID P2), no_permission (other 403: no IdentityRiskyUser.Read.All) or error; licensed and not_licensed are cached for an hour (a 200 whose rows say riskLevel: hidden also means no P2). The endpoint itself is probed: /subscribedSkus can be refused and a SKU list would still have to be mapped to P2. health_check reports it per tenant without changing status, and daily_brief's summary per tenant, so the day P2 appears is noticed. Licensed: microsoft_risky_users lists open (atRisk / confirmedCompromised) high-risk users and, separately, medium, marking those also in spray_breaches / foreign_successes. counts are then distinct accounts over every finding, before the lists are cut: high is any account high here or in Microsoft's high list, medium any medium here or in Microsoft's medium list that is not high (counts.microsoft_high / microsoft_medium say how many Microsoft lists). Otherwise the section is {available: false, reason} and nothing else changes -- never read it as "no risky users".

Setup

bash
uv tool install entraadm-mcp# orpip install entraadm-mcp

Configuration

Set the three app-only variables for production/unattended use:

bash
export ENTRAADM_TENANT_ID=00000000-0000-0000-0000-000000000000export ENTRAADM_CLIENT_ID=00000000-0000-0000-0000-000000000000export ENTRAADM_CLIENT_SECRET=your-client-secret

Or leave all three unset and run az login first for local development.

Optional:

bash
# Default page cap for the log-scanning tools (1-50, default 50).export ENTRAADM_MAX_PAGES_DEFAULT=50# Wall-clock budget per tool call in seconds (default 45; 0 disables). A hosted MCP# client cuts a call off at about 60 s, so a scan stops at the budget and returns# what it has with capped=true.export ENTRAADM_DEADLINE=45# Your own address ranges (NAT egress, offices, labs). Spray suspects inside them are# listed separately as internal_spray_suspects, labelled with the site.export ENTRAADM_SITES_INI=/path/to/sites.ini# The countries your users sign in from (assigned ISO 3166-1 alpha-2 codes). compromise_suspects# reports successes from elsewhere; unset or with an invalid code, that list is skipped.export ENTRAADM_HOME_COUNTRIES=JP# Register revoke_sign_in_sessions, the one state-changing tool (exactly "1"; anything# else leaves it off). Also needs the User.RevokeSessions.All permission.export ENTRAADM_ENABLE_REVOKE_SESSIONS=1# Serve several tenants from one process (see "Several tenants" below).export ENTRAADM_CONFIG=/path/to/tenants.ini

ENTRAADM_SITES_INI uses the same format as keycloak-mcp's KEYCLOAK_SITES_INI, so one file can serve both (see sites.ini.example). Lookups are longest-prefix. A file that is set but cannot be read makes health_check report degraded, and every IP is then treated as external. An invalid CIDR is reported the same way, but the valid ranges are still used.

Several tenants

One process can serve several Entra tenants, the way gwsadm-mcp serves several Workspace domains. Point ENTRAADM_CONFIG at an INI file with one [tenant.<name>] section per tenant:

ini
[tenant.staff]tenant_id = 00000000-0000-0000-0000-000000000001domains = example.edudefault = true
[tenant.students]tenant_id = 00000000-0000-0000-0000-000000000002domains = students.example.edu; optional: an app registration of its own instead of the shared one; client_id = 00000000-0000-0000-0000-000000000003; client_secret_env = ENTRAADM_STUDENTS_CLIENT_SECRET
KeyRequiredMeaning
tenant_idyesThe tenant's id (GUID) or a domain name of it (with a dot, e.g. contoso.onmicrosoft.com); common, organizations and consumers are refused
domainsyesComma-separated UPN domains routed to this tenant (exact match, any case)
defaulton one section when there are severalThe tenant used when a call names none; a single section is the default anyway
client_id + client_secret_envno, both or neitherAn app of its own; client_secret_env names the env var holding its secret. Never put a secret in the file

A section without client_id reuses the env app (ENTRAADM_CLIENT_ID / ENTRAADM_CLIENT_SECRET), with the token requested from that section's tenant; with no env app it uses az login in that tenant. ENTRAADM_TENANT_ID is not used when ENTRAADM_CONFIG is set. Unset ENTRAADM_CONFIG keeps the single-tenant setup above unchanged.

To share one app across tenants, make the app registration multi-tenant (Supported account types: accounts in any organizational directory), add the same Graph application permissions, then have an administrator of each other tenant grant admin consent:

https://login.microsoftonline.com/<tenant-id>/adminconsent?client_id=<app-id>

The server refuses to start, and --check fails, when the file is unreadable, has a section that is not [tenant.*] or an unknown key, misses tenant_id or domains, has a tenant_id that is neither a GUID nor a dotted domain, gives one domain to two tenants, or has no default (or two) among several tenants. --check probes every tenant.

Routing:

  • A tool that takes a UPN (get_user, get_user_auth_methods, signin_logs, revoke_sign_in_sessions, and directory_audits when user is given) works in the tenant whose domains holds the UPN's domain (exact match). A domain no tenant claims is a refusal, not an exception: tenant_configured: false, found: null (no lookup ran) and the configured_domains. For revoke_sign_in_sessions: revoked: false, found: null, nothing is sent. List <tenant>.onmicrosoft.com in domains if such UPNs are looked up often.
  • get_user, get_user_auth_methods and signin_logs also take tenant, which wins over the UPN's domain: for a *.onmicrosoft.com or #EXT# UPN, or a B2B guest whose sign-ins are logged in the host tenant. revoke_sign_in_sessions has no such override on purpose: a write is only ever aimed at the tenant that owns the UPN's domain.
  • A tenant-wide tool (signin_failure_stats, signin_success_stats, compromise_suspects, signin_by_ip, directory_audits, m365_service_health) takes an optional tenant; default the default tenant. directory_audits with both tenant and a user whose domain another tenant claims is refused (it would search the wrong log and find nothing); a user whose domain no tenant claims (an external or partner admin, a guest) is searched in the given or default tenant, and the result says user_domain_claimed: false.
  • audit_log_search_start picks its tenant like directory_audits (tenant, else the tenant claiming user's domain, else the default). audit_log_search_result takes tenant and must get the one the start call used: a search exists only in the tenant that created it (elsewhere it is found: false).
  • tenant is a section name, one of its domains, or its tenant id. Without a tenant file it may be default or the env tenant id; anything else is refused (tenant_configured: false).
  • health_check and daily_brief cover every tenant. Their top-level keys stay the default tenant's, as before; tenant names it and tenants holds the per-tenant results. The call budget (ENTRAADM_DEADLINE) is split evenly among the tenants (default first, each taking a share of what is left), so a slow tenant cannot stall the others; a tenant that does not answer is an error in its own entry, and makes health_check's status at most degraded. With ENTRAADM_DEADLINE=0 there is no budget: every tenant is scanned in full, one after another, which can outlast a client's ~60 s cutoff.
  • The Azure Resource Manager tools (list_subscriptions, vm_backup_status, vm_backup_policies, vm_activity) serve the default tenant only.
  • With a tenant file, every result names its tenant (the ARM tools: the default tenant; a refusal that picked none: null). Env-only results are unchanged, except that revoke_sign_in_sessions always carries tenant, tenant_configured and configured_domains (its keys never vary).

Usage

Claude Code (plugin)

/plugin marketplace add shigechika/entraadm-mcp/plugin install entraadm-mcp@entraadm-mcp

Claude Code (manual)

Add to .mcp.json:

json
{  "mcpServers": {    "entraadm-mcp": {      "type": "stdio",      "command": "uvx",      "args": ["entraadm-mcp"],      "env": {        "ENTRAADM_TENANT_ID": "${ENTRAADM_TENANT_ID:-}",        "ENTRAADM_CLIENT_ID": "${ENTRAADM_CLIENT_ID:-}",        "ENTRAADM_CLIENT_SECRET": "${ENTRAADM_CLIENT_SECRET:-}"      }    }  }}

Codex

bash
codex mcp add entraadm-mcp \  --env ENTRAADM_TENANT_ID=your-tenant-id \  --env ENTRAADM_CLIENT_ID=your-client-id \  --env ENTRAADM_CLIENT_SECRET=your-client-secret \  -- uvx entraadm-mcp

Any other MCP client that launches stdio servers works the same way: give it uvx entraadm-mcp as the command and set the ENTRAADM_* variables from Configuration in its environment.

Direct execution

bash
entraadm-mcp

CLI options

OptionEffect
--versionPrint the version and exit
--checkResolve auth, probe Graph reachability and sign-in log access, print a report (plus an informational ARM line), exit 0 (or 1 on config error)
archive-signins --dir DIRKeep the sign-in log past Entra's 30 days: write each ended UTC slot to DIR/<tenant>/<slot>.jsonl.gz (run it daily; see the reference)

Notes

  • Coverage contract. Every result that walks a paged Graph collection carries a capped boolean when its window was not fully scanned — a partial scan is never reported as if it were exhaustive.
  • found: false is not an error. get_user and get_user_auth_methods answer a nonexistent account with {"found": false, ...}, not an error key — a typo'd userPrincipalName should never look like this server being broken.
  • Retention. Entra ID P1 retains sign-in and directory audit logs for 30 days. A window beyond that returns an empty result, not an error.

Development

bash
uv sync --devuv run pytest -vuv run ruff check .uv run ruff format --check .

Live smoke test

bash
uv run python scripts/smoke_test.py

Read-only (revoke_sign_in_sessions is skipped by name even when enabled), no payloads printed (tool names/statuses/row counts only), and bounded (small explicit windows/page caps) — nothing here writes to the tenant or scans more than a day of logs.

Releasing

This repository uses release-please driven by Conventional Commits. Merge a feat:/fix: PR to main, and release-please opens (or updates) a release PR; merging that PR tags a release and triggers the publish pipeline (PyPI, MCP Registry).

License

MIT

来源:README.md,提交 3b6fcf0

工具

17

health_check

Fleet-standard health probe: service/version/status plus two independent Graph probes. ``graph`` confirms Microsoft Graph is reachable at all (``GET /users`` with ``$top=1`` -- needs only ``User.Read.All``, the minimum permission every deployment of this server needs anyway). ``signin_probe`` additionally confirms the current credential can read sign-in logs -- the permission every other tool here except ``get_user`` depends on. Both probes always run, independently of each other: a tenant that has AuditLog.Read.All but not (yet) the baseline User.Read.All would otherwise have this report "Graph unreachable" -- a fabricated diagnosis, since Graph plainly *is* reachable if the other probe succeeds. ``status`` is derived from the two outcomes: ``healthy`` when both succeed, ``degraded`` when exactly one does (Graph is reachable but some permission is missing), ``error`` only when neither does. Read-only. Always returns the same keys regardless of outcome (``detail`` is null on success, a translated message on failure), so a caller never has to branch on which keys are present. ``sites`` reports the optional ENTRAADM_SITES_INI table (``configured``, ``ranges``, ``error``). A configured file that fails to load is an ``error`` here and leaves ``status`` no better than ``degraded``: without it every internal IP would be reported as an external spray suspect. With a tenant file (``ENTRAADM_CONFIG``) every tenant is probed. The top-level keys stay the DEFAULT tenant's, as in a single-tenant deployment, plus ``tenant`` (the default's name) and ``tenants``: ``{name: {default, domains, auth_mode, status, graph, signin_probe}}`` for every tenant, the default included. Each tenant's probes run under an even share of the call budget (``ENTRAADM_DEADLINE``), so a tenant that does not answer is an ``error`` there instead of stalling the call. Another tenant that is no

无需输入参数。

get_user

One account's identity/lifecycle state -- the first thing to check on any triage report. ``account_enabled=false`` means the account itself is the whole story; stop there. A stale ``last_password_change_date_time`` alongside a fresh "wrong password" complaint (AADSTS50126 in ``signin_logs``) is the most common on-the-ground pattern: the password changed or expired somewhere, and a cached credential on one device is now stale. ``on_premises_sync_enabled=true`` means this account is synced from an on-premises directory (Entra Connect) -- Entra is a downstream copy of its password via Password Hash Sync, not the source of truth. ``licenses`` names are resolved from the tenant's SKU catalog (``/subscribedSkus``, page budget from ``ENTRAADM_MAX_PAGES_DEFAULT``); ``licenses_capped: true`` appears only when that scan was cut short before resolving one of this account's own licenses -- when present, one or more ``licenses`` entries is a raw skuId rather than a friendly name. ``sign_in_activity`` needs an additional Graph read (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role) beyond what the rest of this tool needs. If that permission is missing, every other field above still returns and ``sign_in_activity`` alone degrades to ``{"error": ..., "missing_permission": "AuditLog.Read.All"}``. A nonexistent account is a normal answer, not a tool failure: the result is ``{"found": false, "user_principal_name": upn}`` rather than an ``error`` key, so a typo'd UPN in a triage report cannot be mistaken for this tool being broken. Read-only (User.Read.All application permission, or an equivalent delegated read). Requires an exact userPrincipalName, not a display name or partial match. Args: upn: The account's userPrincipalName, e.g. "[email protected]". tenant: Search this tenant instead of the one cl

输入参数upn必填tenant

signin_logs

One user's recent sign-in events, AADSTS-annotated. The most direct answer to "why can't this person log in": each entry's ``error_code_meaning`` translates the raw AADSTS code (e.g. 50126 -> "invalid credentials (wrong password)") so triage rarely needs a second lookup. ``result`` filters client-side after the Graph fetch (Graph cannot filter sign-ins on status/errorCode server-side): "failure" (the default) keeps only failed attempts, "success" keeps only clean ones, "all" keeps everything. Because the filter is client-side, this walks pages until it has collected ``top`` matching entries or exhausts ``max_pages`` -- a mostly- successful user can otherwise mean paging through hundreds of rows to find a handful of failures. ``capped=true`` means the page budget ran out (or ``top`` was reached) before the whole window was scanned; a low match count alongside ``capped=true`` is evidence of "no more found within the budget", not "no more exist". Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role). Entra ID P1 retains sign-in logs for 30 days; ``hours`` beyond that returns an empty result, not an error. Args: user: The account's userPrincipalName. hours: How far back to look, clamped to [1, 720] (30 days). result: "failure" (default), "success", or "all". top: Maximum matching entries to return, clamped to [1, 500]. max_pages: Page budget for the client-side filter walk (default: ENTRAADM_MAX_PAGES_DEFAULT). tenant: Search this tenant instead of the one claiming the UPN's domain (section name, domain or tenant id): for a <tenant>.onmicrosoft.com or #EXT# UPN, or a B2B guest's sign-ins in the host tenant.

输入参数topuser必填hoursresulttenantmax_pages

signin_by_ip

Every sign-in from one source IP: who got in from it, who was tried, and when. The follow-up to a ``spray_suspects`` or ``shared_ips`` hit: Graph can filter sign-ins on ``ipAddress`` server-side, so this is one cheap query, not a log walk. ``users`` summarises the IP per account (successes, failures, first/last seen, up to 50 accounts) over every row fetched; ``events`` lists the newest ``top`` entries that match ``result`` ("all" by default, or "success" / "failure"), each carrying the account name and the same AADSTS annotation as ``signin_logs``. ``capped=true`` means the page budget or the deadline ran out before the window was fully read; ``events_truncated=true`` means more matching rows were read than ``top`` returns (the ``users`` summary still counts them). Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role). Args: ip: The source IPv4 or IPv6 address, exactly as the sign-in log shows it. hours: How far back to look, clamped to [1, 720] (30 days). result: "all" (default), "success", or "failure" -- which events to list. top: Maximum events to return, clamped to [1, 500]. max_pages: Page budget (default: ENTRAADM_MAX_PAGES_DEFAULT). tenant: Tenant to read: a [tenant.<name>] section name or one of its domains (default: the default tenant).

输入参数ip必填tophoursresulttenantmax_pages

signin_failure_stats

Tenant-wide sign-in failure aggregation -- the Entra ID counterpart to the RADIUS failure patrol. Time-bounded (ENTRAADM_DEADLINE, default 45 s): on a wide window or a busy day the scan stops early and ``capped=true`` marks the counts as a lower bound; narrow ``hours`` for a full count. Aggregates failed sign-ins across the whole tenant into four views: top AADSTS error codes (with the same meaning annotations as ``signin_logs``), top failing users, top applications, and top source IPs. ``spray_suspects`` flags any IP with failed sign-ins against 5 or more distinct users -- Entra's smart lockout is per-account, so a low-and-slow password spray from one IP across many accounts does not trip it the way a brute force against one account does; this is the observation a per-account view cannot make on its own. This mirrors the KeyCloak-side spray detection this fleet already relies on; neither the official Microsoft MCP Server for Enterprise nor Graph itself offers this aggregation. When ENTRAADM_SITES_INI names the tenant's own address ranges, IPs inside them get ``site`` (and ``site_name``) in ``top_ips``, and suspects inside them move from ``spray_suspects`` to ``internal_spray_suspects``: a NAT egress or a room of shared workstations fails for many users every day, and would otherwise bury an outside source. Internal suspects are kept, not dropped, because an internal host can be compromised too. ``sites`` reports whether the file loaded; an invalid CIDR is reported in ``sites.error`` while the valid ranges are still used, and a file that cannot be read leaves every IP in ``spray_suspects``. Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role). Graph cannot filter sign-ins on status/errorCode server-side, so this walks up to ``max_pages`` of the full sign-in log for the window and aggregates cli

输入参数hourstenantmax_pages

signin_success_stats

Tenant-wide *successful* sign-in aggregation by source IP -- the view that finds a breach. ``signin_failure_stats`` shows who is being attacked; this shows whether anyone got in. The breach signature is one source IP signing in successfully as several different accounts, most often over a legacy protocol (``clientAppUsed`` such as "Authenticated SMTP" or "IMAP4", which carry no MFA). ``shared_ips`` lists the IPs with successes for ``min_distinct_users`` or more distinct accounts, most-shared first (up to 50 IPs, ``shared_ips_capped`` when more qualified; account names up to 25 per IP), with the client apps and the countries seen. ``legacy_auth_users`` lists the accounts that succeeded over a legacy protocol at all, with how many IPs and countries they came from. A campus NAT, a VDI farm or a shared proxy also puts many accounts behind one IP, so a shared IP is a lead, not a verdict: the caller excludes its own egress ranges and reads the client apps and countries before calling anything a breach. Graph cannot filter sign-ins on ``status/errorCode`` server-side, so like ``signin_failure_stats`` this walks the sign-in log for the window and aggregates client-side. The walk covers interactive sign-ins only (Graph's default listing): every legacy-protocol authentication is logged as interactive, so none is missed, but non-interactive token refreshes are not counted; ``capped=true`` means the page budget or the deadline (ENTRAADM_DEADLINE, default 45 s) ran out first and the counts are a lower bound -- narrow ``hours`` for a full count. Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role). Args: hours: How far back to look, clamped to [1, 720] (30 days). max_pages: Page budget (default: ENTRAADM_MAX_PAGES_DEFAULT). min_distinct_users: Distinct accounts an IP needs to appear in

输入参数hourstenantmax_pagesmin_distinct_users

compromise_suspects

Accounts that were probably taken over, from one walk of the sign-in log. The success-side counterpart of ``signin_failure_stats``' spray suspects. Two findings, both from EXTERNAL source IPs (outside the address ranges in ENTRAADM_SITES_INI; without that file every IP counts as external): - ``spray_breaches`` (severity ``high``): an IP with credential failures (wrong password, unknown, locked or disabled account -- not MFA prompts, session interrupts or Conditional Access blocks, which follow a correct password) for ``min_spray_users`` or more distinct accounts in the window AND a successful sign-in as at least one account. Without internal ranges (ENTRAADM_SITES_INI unset or unreadable, ``internal_ranges_known: false``) these are ``medium`` with a ``note``: the tenant's own NAT egress can look like a spray source. One row per account and IP: ``failed_distinct_users`` from that IP, first/last success, client apps, countries, ``legacy_auth``. The strongest signal: a spray that found a working password. - ``foreign_successes``: successes from a country outside ENTRAADM_HOME_COUNTRIES (assigned ISO 3166-1 alpha-2 codes, e.g. "JP"; one invalid code disables this list, see ``home_countries.error``), grouped per account with countries, the most used IPs (10, with counts), client apps, first/last, and ``home_success_in_window`` (the account also signed in from a home country or an internal IP in the window; null when not seen in a capped scan, since the unread part may hold it). ``high`` when a legacy protocol was used (password only, no MFA), the account is also a high ``spray_breaches`` entry, the success came through Azure CLI / Azure PowerShell / Azure AD PowerShell (``admin_tools``; how stolen passwords are tested and used), or the successes span 3 or more countries within 24 hours (a rotating proxy pool, not a trav

输入参数hourstenantmax_pagesmin_spray_users

directory_audits

Who did what to the directory, and when -- the operator-side counterpart to signin_logs. Every admin action against a user object (block/unblock, password reset, role assignment, attribute edits) appears here, naming the actor (``initiated_by``) and the affected object(s) (``target_resources``). This is the record a manual "unblock and reset" intervention -- like the one that closed the 2026-08-21 case this server exists to shorten -- leaves behind; it is how a later triage can tell "already handled by a human" from "still open". ``user``, when given, matches audits where that account is either the initiator or a target resource. Graph's directoryAudits endpoint only supports server-side ``$filter`` on the *initiator* (``initiatedBy/user/userPrincipalName``), not on ``targetResources``, so this fetches the full time window and matches both sides client-side -- a window with many unrelated admin actions can need a larger ``max_pages`` budget than ``signin_logs``/``signin_failure_stats`` to find one specific user's audits; ``capped=true`` warns when that budget ran out before the window was fully scanned. Read-only (AuditLog.Read.All application permission, or -- for azure-cli auth -- the Reports Reader directory role). Entra ID retains directory audit logs for 30 days, same as sign-in logs. Time-bounded: the scan stops after ENTRAADM_DEADLINE seconds (default 45) and returns what it has with ``capped=true``. Over several days the log is dominated by device-registration noise ("Update device"), so pass ``category`` (for example ``UserManagement``, ``RoleManagement``, ``GroupManagement``, ``ApplicationManagement``) to have Graph filter server-side; that keeps a multi-day window inside the budget. Args: category: Only this Graph audit category (letters only; default: all). user: Restrict to audits naming this userPrincipalName as actor or target (

输入参数topuserhourstenantcategorymax_pages

get_user_auth_methods

Registered authentication methods for one account -- is MFA actually set up? ``mfa_registered`` answers "would this account survive a password-spray hit": True iff at least one non-password method is registered (Authenticator app, phone, FIDO2 security key, Windows Hello, a temporary access pass, software OATH token, or a platform credential/passkey). ``password`` itself is excluded from that count -- every account has one, so its presence alone says nothing about MFA coverage. A nonexistent account is a normal answer, not a tool failure: the result is ``{"found": false, "user_principal_name": upn}`` rather than an ``error`` key, matching ``get_user``'s contract. Read-only (UserAuthenticationMethod.Read.All application permission). This endpoint is app-only only: it is not exposed to delegated (azure-cli) auth under this tenant's current role assignment, so it degrades to a permission error under azure-cli auth even when other tools work. Args: upn: The account's userPrincipalName. tenant: Search this tenant instead of the one claiming the UPN's domain (section name, domain or tenant id): for a <tenant>.onmicrosoft.com or #EXT# UPN, or a B2B guest's sign-ins in the host tenant.

输入参数upn必填tenant

list_subscriptions

The Azure subscriptions this credential can see, as ARM lists them. Azure Resource Manager in the DEFAULT tenant only, also when several tenants are configured (``ENTRAADM_CONFIG``). Read-only (``GET /subscriptions``; needs no RBAC beyond being able to see a subscription at all). The discovery anchor for ``vm_backup_status`` and ``vm_backup_policies``: both accept a subscription id or display name from this list. An empty list with ``auth`` working means no Reader role has been granted anywhere yet. ``capped`` marks a listing cut short by the page budget.

输入参数max_pages

vm_backup_status

Which Azure VMs are backed up by Azure Backup, and whether the last backup succeeded. Azure Resource Manager in the DEFAULT tenant only, also when several tenants are configured (``ENTRAADM_CONFIG``). Joins every VM of a subscription (``Microsoft.Compute/virtualMachines``) with the Azure-VM protected items of its Recovery Services vaults, on the VM's ARM id (case-insensitively; ARM casing differs between the two services). Read-only; needs the Reader role on the subscription. Per VM: - ``protected``: ``true`` when a protected item exists with ``protectionState`` ``Protected`` or ``IRPending`` (first backup not done yet, ``note`` says so); ``false`` when there is none, or it is ``ProtectionStopped`` / ``ProtectionPaused`` / ``ProtectionError`` (data may be retained, backups are not running -- the state is shown); ``null`` (unknown) when no item was found but the vault scan was incomplete (``capped``, a skipped vault, an unreadable or unreached subscription -- in the global join, any subscription's) -- a partial scan never produces a false "unprotected". - ``backup_ok``: ``true`` for ``lastBackupStatus`` ``Completed`` / ``CompletedWithWarnings``, ``false`` for ``Failed``, ``null`` before any backup ran or while one is running (``InProgress``, ``Warning``, an unknown value). An item whose ``protectionState`` is missing is ``protected: null`` with a note. When two vaults hold an item for one VM, the one still backing up wins (``duplicate_items`` counts them). - ``stale``: no recovery point (``lastRecoveryPoint``, the proof of a successful backup) within ``stale_hours``. The default 36 h suits daily policies; raise it for weekly ones (``policy_name`` is in the row). ``subscription`` is an id or display name from ``list_subscriptions``; ``None`` scans every visible subscription and joins globally, so a VM backed up to a vault i

输入参数max_pagesstale_hourssubscription

vm_backup_policies

Schedule and retention of the Azure VM backup policies in each Recovery Services vault. Azure Resource Manager in the DEFAULT tenant only, also when several tenants are configured (``ENTRAADM_CONFIG``). Read-only; needs the Reader role on the subscription. Per policy: ``policy_type`` (``V1`` = ``SimpleSchedulePolicy``, ``V2`` = ``SimpleSchedulePolicyV2`` with an hourly / daily / weekly schedule), ``schedule`` (frequency, run times in UTC, weekdays, or the hourly window), ``retention`` (``LongTermRetentionPolicy``: daily / weekly / monthly / yearly blocks with their day selection and duration), ``instant_rp_days``, ``timezone`` and ``protected_item_count`` (ARM's own count). A schedule or retention type this server does not know is returned as ``{"type": <name>, "unparsed": true}``, never dropped. ``subscription`` is an id or display name from ``list_subscriptions`` (``None`` = every visible subscription); an unknown or ambiguous name returns ``found: false``. Coverage flags as in ``vm_backup_status``: ``skipped_vaults`` per subscription, ``skipped`` subscriptions when the budget ran out, ``capped`` overall.

输入参数max_pagessubscription

vm_activity

What happened to an Azure VM in a time window: operations people ran, and events Azure raised. Azure Resource Manager in the DEFAULT tenant only, also when several tenants are configured (``ENTRAADM_CONFIG``). Answers "did someone restart this VM, or did Azure?" from the Azure activity log of the VM's subscription. Read-only; needs the Reader role on the subscription. Two reads: the VM's resource group (server-side filter, so a busy subscription cannot crowd the VM's own events out of the page budget) feeds ``health`` and ``vm_operations``; the whole subscription feeds the rest. - ``health``: Resource Health events of the VM (``cause`` ``PlatformInitiated`` = Azure, e.g. ``Redeploying due to host failure``; ``UserInitiated`` = an operation on the VM, or ``Reboot initiated from inside the machine`` for a restart from the guest OS). - ``vm_operations``: Administrative events on the VM and its sub-resources (restart, deallocate, redeploy, extension changes), with ``caller`` (a user principal name or an application id). One operation usually logs several events (Started, Accepted, Succeeded) sharing a ``correlation_id``. - ``other_operations``: Administrative events on any other resource of the subscription (``count`` of events, ``operations`` = distinct correlation ids, ``items`` = the newest 50). Zero here and in ``vm_operations`` means nobody operated the subscription through Azure Resource Manager in the window; it says nothing about actions inside the guest OS. - ``service_health``: Azure incidents and planned maintenance announced to the subscription. - ``other_categories``: counts of the remaining categories (Alert, Policy, other resources' health events, ...). ``vm`` is the VM name, matched case-insensitively across the subscriptions this credential can see; narrow it with ``subscription`` (id or display name from ``

输入参数vm必填endhoursstartmax_pagessubscriptionresource_group

m365_service_health

Microsoft 365 service status per service, and the open incidents and advisories. Two Graph reads (``ServiceHealth.Read.All``): ``healthOverviews`` (one row per service with its current status) and the unresolved ``serviceAnnouncement/issues``, newest first. A sign-in failure spike reads very differently while Entra or Exchange has an open incident, which is why ``daily_brief`` carries this section too. Each section degrades on its own (``{"error", "missing_permission"}``); ``summary`` is computed from whatever came back. ``capped`` means either collection was cut short by the page budget, so the counts are lower bounds. The message center (``messages``) needs a different permission and is not read. Args: max_pages: Page budget for each collection (default: ENTRAADM_MAX_PAGES_DEFAULT). tenant: Tenant to read: a [tenant.<name>] section name or one of its domains (default: the default tenant).

输入参数tenantmax_pages

audit_log_search_start

Start a Microsoft Purview audit log search: what was done in OneDrive/SharePoint, Exchange, Entra. The follow-up to a confirmed takeover: the sign-in log shows which resources a session obtained tokens for, this shows what it then did (files opened or downloaded, mailbox rules and cmdlets, directory changes). A search runs asynchronously in Microsoft 365 and usually takes several minutes, so this only submits it (Graph ``POST /security/auditLog/queries``) and returns ``query_id``; call ``audit_log_search_result`` with that id (and the same ``tenant``) until ``done`` is true. The call creates a saved audit search object in the tenant (listed in the Purview audit search page); it changes no directory, mailbox or file data. Each call counts against the tenant's audit search limits (searches per day, searches running at once): a 429 comes back as ``rate_limited: true`` and is not retried. At least one filter besides the window is required, so a call never searches the whole tenant's log. The filters combine with AND. Needs the application permission ``AuditLogsQuery.Read.All``, or the service-scoped ``AuditLogsQuery-OneDrive.Read.All`` / ``-SharePoint.Read.All`` / ``-Exchange.Read.All`` / ``-Entra.Read.All`` (then set ``service`` to match). A 403 returns ``missing_permission``. ``created`` is true, false (nothing was created), or null (the answer was lost; a search may exist anyway). Args: user: Only records of this userPrincipalName. hours: How far back from ``end`` (or now) to search, clamped to [1, 4320] (180 days). Ignored with ``start``. start: Window start, ISO 8601 with an offset (e.g. 2026-10-21T22:00:00+09:00). end: Window end, ISO 8601 with an offset (default: now). record_types: auditLogRecordType values, e.g. ["sharePointFileOperation", "exchangeItem"]. operations: Operation names, e.g. ["FileDownloaded", "New-InboxRule", "MailI

输入参数enduserhoursstarttenantkeywordserviceobject_idsoperationsip_addressesrecord_types

audit_log_search_result

Status and, once finished, the summarised records of an audit search started by audit_log_search_start. Reads Graph ``GET /security/auditLog/queries/{id}``. While ``status`` is ``notStarted`` or ``running`` the answer is the status only (``done: false``); call again in a minute or two. ``failed``, ``cancelled`` and any status this server does not know are ``done: true`` with an ``error``. Once ``succeeded``, the records are paged (``/records``) and come back as: - ``summary``: ``total_records``, ``first_seen``/``last_seen``, counts by operation, service, record type and user, and per client IP the count with first/last seen (top 25 of each list; ``distinct`` has the full counts); - ``records``: the newest ``top`` records, each with time, operation, service, record type, user, client IP, object id and a few ``details`` from ``auditData`` (client IP, user agent, result status, site URL, file name, the names of cmdlet parameters -- never their values), every string cut to 200 characters. The full ``auditData`` is not returned. ``capped=true`` means the counts are lower bounds: either the page budget or the deadline ran out before every record was read (call again with a larger ``max_pages``), or ``query.record_count_limit_exceeded`` is true -- Graph itself stopped collecting at its per-search record limit (``query.record_count_limit``), so the search holds only part of the matching records whatever ``max_pages`` is; narrow the window or the filters and start a new search. ``records_truncated`` means more records were read than ``top`` returns (the summary counts them all). An unknown id is ``found: false``: a search exists only in the tenant that created it, so pass the ``tenant`` its start call used. Permissions as audit_log_search_start; a 403 returns ``missing_permission``. Args: query_id: The ``query_id`` returned by audit_log_search_start (a

输入参数toptenantquery_id必填max_pages

daily_brief

One-call morning-patrol summary: M365 service health, sign-in failures, spray suspects, admin actions. Combines ``m365_service_health``, ``signin_failure_stats``, ``directory_audits`` and ``compromise_suspects`` (its counts and the top 5 of each list; ``summary.compromise_suspects`` has the high/medium counts) into one result with a compact ``summary`` on top. The failure stats and the compromise suspects come from one walk of the sign-in log. The shape matches this fleet's other ``daily_brief`` tools. A permission failure in one section degrades only that section's contribution to ``summary`` -- the other sections still return in full. Azure VM backup is deliberately not a section here: its ARM fan-out is the one scan that could exhaust the shared budget, so call ``vm_backup_status`` separately. With a tenant file (``ENTRAADM_CONFIG``) it covers every tenant: the top-level keys stay the DEFAULT tenant's brief, exactly as in a single-tenant deployment, plus ``tenant`` (the default's name) and ``tenants`` (``{name: the same brief}`` for each other tenant). The call budget (``ENTRAADM_DEADLINE``) is split: default first, each tenant gets an even share of what is left, so a busy tenant cannot starve the next one (a tenant that finishes early leaves its rest to the others). With ``ENTRAADM_DEADLINE=0`` there is no budget and every tenant is scanned in full, one after another, which can outlast a client's ~60 s cutoff. Env-only results carry neither key. Runs both sections synchronously in one tool call, unlike the sibling gwsadm-mcp's job+poll ``daily_brief``. If this proves too slow for a tenant's sign-in volume against the client's tool-call timeout, port that job+poll pattern here (tracked in this repo's CLAUDE.md Roadmap). Args: hours: How far back to look, clamped to [1, 720] (30 days). max_pages: Page budget passed to both sections (default:

输入参数hourssamplesmax_pages

版本历史

11
  1. v1.10.1最新Nov 1, 2026
  2. v1.10.0Nov 1, 2026
  3. v1.9.0Oct 31, 2026
  4. v1.8.1Oct 26, 2026
  5. v1.8.0Oct 24, 2026
  6. v1.4.0Oct 22, 2026
  7. v1.3.0Oct 15, 2026
  8. v1.2.0Oct 14, 2026
  9. v1.1.0Oct 2, 2026
  10. v1.0.1Sep 29, 2026
  11. v0.1.0Sep 16, 2026

评分

已满足适用检查项的 90%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分
  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分