L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

performing-oauth-scope-minimization-review

performing-oauth-scope-minimization-review

执行 OAuth 2.0 权限范围最小化审查,识别过度授权的第三方应用集成、 过多的 API 范围、未使用的令牌授权以及跨身份提供商和 SaaS 平台的 高风险 OAuth 同意模式。 适用于 OAuth 范围审计、API 权限审查、第三方应用风险评估或同意授权最小化的请求。

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: performing-oauth-scope-minimization-review description: > 执行 OAuth 2.0 权限范围最小化审查,识别过度授权的第三方应用集成、 过多的 API 范围、未使用的令牌授权以及跨身份提供商和 SaaS 平台的 高风险 OAuth 同意模式。 适用于 OAuth 范围审计、API 权限审查、第三方应用风险评估或同意授权最小化的请求。 domain: cybersecurity subdomain: identity-access-management tags: [OAuth, scope-minimization, API-security, consent-review, third-party-risk, token-audit] version: "1.0" author: mahipal license: Apache-2.0

执行 OAuth 权限范围最小化审查

适用场景

  • 对第三方应用 OAuth 权限的年度或季度审查
  • 涉及 OAuth 令牌泄露或未授权数据访问的安全事件发生后
  • 需要记录第三方数据访问情况的合规审计(GDPR 第 28 条、SOC 2)
  • 发现影子 IT 应用通过 OAuth 授权访问组织数据
  • 需要权限清理的 SaaS 应用迁移或整合
  • 为 API 集成实施最小权限原则

不适用于审查同一信任边界内第一方应用的权限;OAuth 范围最小化专注于第三方和跨边界的同意授权。

前提条件

  • 对身份提供商(Microsoft Entra ID、Okta、Google Workspace)的管理员访问权限
  • Microsoft Graph API 权限:Application.Read.All、OAuth2PermissionGrant.ReadWrite.All
  • 来自采购或 IT 治理的已批准第三方集成清单
  • OAuth 范围风险分类框架
  • 令牌分析工具(手动审查使用 jwt.io,批量分析使用自动化脚本)

工作流程

步骤 1:清点所有 OAuth 授权和同意权限

枚举所有 OAuth 应用注册及委托权限:

"""
OAuth 授权清点 - Microsoft Entra ID
枚举所有应用注册、服务主体、
委托权限及应用权限授予情况。
"""
import requests
import json
from collections import defaultdict

class EntraOAuthAuditor:
    def __init__(self, tenant_id, client_id, client_secret):
        self.tenant_id = tenant_id
        self.base_url = "https://graph.microsoft.com/v1.0"
        self.token = self._get_token(client_id, client_secret)
        self.headers = {"Authorization": f"Bearer {self.token}"}

    def _get_token(self, client_id, client_secret):
        url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token"
        response = requests.post(url, data={
            "grant_type": "client_credentials",
            "client_id": client_id,
            "client_secret": client_secret,
            "scope": "https://graph.microsoft.com/.default"
        })
        return response.json()["access_token"]

    def get_all_service_principals(self):
        """获取所有服务主体(企业应用)。"""
        apps = []
        url = f"{self.base_url}/servicePrincipals?$top=999&$select=id,appId,displayName,appOwnerOrganizationId,servicePrincipalType,accountEnabled,createdDateTime"

        while url:
            response = requests.get(url, headers=self.headers)
            data = response.json()
            apps.extend(data.get("value", []))
            url = data.get("@odata.nextLink")

        return apps

    def get_oauth2_permission_grants(self):
        """获取所有委托权限授予(用户同意)。"""
        grants = []
        url = f"{self.base_url}/oauth2PermissionGrants?$top=999"

        while url:
            response = requests.get(url, headers=self.headers)
            data = response.json()
            grants.extend(data.get("value", []))
            url = data.get("@odata.nextLink")

        return grants

    def get_app_role_assignments(self, sp_id):
        """获取服务主体的应用权限分配。"""
        url = f"{self.base_url}/servicePrincipals/{sp_id}/appRoleAssignments"
        response = requests.get(url, headers=self.headers)
        return response.json().get("value", [])

    def build_permission_inventory(self):
        """构建全面的 OAuth 权限清单。"""
        service_principals = self.get_all_service_principals()
        delegated_grants = self.get_oauth2_permission_grants()

        # 将服务主体 ID 映射到名称
        sp_map = {sp["id"]: sp for sp in service_principals}

        inventory = []

        # 处理委托权限
        for grant in delegated_grants:
            sp = sp_map.get(grant["clientId"], {})
            scopes = grant.get("scope", "").split()

            for scope in scopes:
                if not scope:
                    continue
                inventory.append({
                    "app_name": sp.get("displayName", "Unknown"),
                    "app_id": grant.get("clientId"),
                    "publisher_tenant": sp.get("appOwnerOrganizationId"),
                    "is_third_party": sp.get("appOwnerOrganizationId") != self.tenant_id,
                    "permission_type": "Delegated",
                    "scope": scope,
                    "consent_type": grant.get("consentType"),  # AllPrincipals 或 Principal
                    "principal_id": grant.get("principalId"),
                    "granted_date": sp.get("createdDateTime"),
                    "is_enabled": sp.get("accountEnabled", True)
                })

        # 处理应用权限
        for sp in service_principals:
            app_roles = self.get_app_role_assignments(sp["id"])
            for role in app_roles:
                inventory.append({
                    "app_name": sp.get("displayName"),
                    "app_id": sp.get("id"),
                    "publisher_tenant": sp.get("appOwnerOrganizationId"),
                    "is_third_party": sp.get("appOwnerOrganizationId") != self.tenant_id,
                    "permission_type": "Application",
                    "scope": role.get("appRoleId"),
                    "consent_type": "AdminConsent",
                    "granted_date": role.get("createdDateTime"),
                    "is_enabled": sp.get("accountEnabled", True)
                })

        return inventory

步骤 2:按风险等级对 OAuth 范围分类

根据数据访问敏感性对权限进行分类:

"""
OAuth 范围风险分类
根据数据敏感性和访问广度将 API 范围映射到风险等级。
"""

MICROSOFT_GRAPH_SCOPE_RISK = {
    # 严重 - 完全管理或不受限制的访问
    "critical": {
        "scopes": [
            "Directory.ReadWrite.All",
            "RoleManagement.ReadWrite.Directory",
            "Application.ReadWrite.All",
            "AppRoleAssignment.ReadWrite.All",
            "Mail.ReadWrite",
            "Mail.Send",
            "Files.ReadWrite.All",
            "Sites.FullControl.All",
            "User.ReadWrite.All",
            "Group.ReadWrite.All",
            "MailboxSettings.ReadWrite",
            "full_access_as_app",
        ],
        "risk_description": "可读写所有数据、修改目录或模拟用户",
        "review_frequency": "每月",
        "requires_admin_consent": True
    },
    # 高 - 广泛读取访问或敏感数据写入
    "high": {
        "scopes": [
            "Mail.Read",
            "Mail.Read.Shared",
            "Calendars.ReadWrite",
            "Contacts.ReadWrite",
            "Files.Read.All",
            "Sites.Read.All",
            "User.Read.All",
            "Group.Read.All",
            "Directory.Read.All",
            "AuditLog.Read.All",
            "SecurityEvents.ReadWrite.All",
            "TeamSettings.ReadWrite.All",
        ],
        "risk_description": "对组织敏感数据具有广泛读取访问",
        "review_frequency": "每季度",
        "requires_admin_consent": True
    },
    # 中 - 范围限定的数据访问
    "medium": {
        "scopes": [
            "Calendars.Read",
            "Contacts.Read",
            "Files.ReadWrite",
            "Sites.ReadWrite.All",
            "Tasks.ReadWrite",
            "Notes.ReadWrite.All",
            "Chat.ReadWrite",
            "ChannelMessage.Send",
            "Team.ReadBasic.All",
        ],
        "risk_description": "对特定数据类型具有写入能力的范围限定访问",
        "review_frequency": "每半年"
    },
    # 低 - 最小或仅用户配置文件访问
    "low": {
        "scopes": [
            "User.Read",
            "openid",
            "profile",
            "email",
            "offline_access",
            "Calendars.Read.Shared",
            "People.Read",
            "User.ReadBasic.All",
        ],
        "risk_description": "基本用户配置文件或最小范围限定访问",
        "review_frequency": "每年"
    }
}

def classify_scope_risk(scope):
    """对单个 OAuth 范围按风险等级分类。"""
    for risk_level, config in MICROSOFT_GRAPH_SCOPE_RISK.items():
        if scope in config["scopes"]:
            return {
                "scope": scope,
                "risk_level": risk_level,
                "description": config["risk_description"],
                "review_frequency": config["review_frequency"]
            }
    # 未知范围默认为高风险
    return {
        "scope": scope,
        "risk_level": "high",
        "description": "未知范围 - 需要手动分类",
        "review_frequency": "每季度"
    }

def analyze_app_risk(app_permissions):
    """计算应用权限的综合风险评分。"""
    risk_weights = {"critical": 40, "high": 20, "medium": 10, "low": 2}
    total_score = 0
    classified_scopes = []

    for perm in app_permissions:
        classification = classify_scope_risk(perm["scope"])
        classified_scopes.append(classification)
        total_score += risk_weights.get(classification["risk_level"], 10)

    # 应用类型权限(vs 委托权限)的额外风险加分
    app_type_permissions = [p for p in app_permissions if p["permission_type"] == "Application"]
    total_score += len(app_type_permissions) * 15

    # 管理员同意的广泛访问的额外风险加分
    admin_consent = [p for p in app_permissions if p["consent_type"] == "AllPrincipals"]
    total_score += len(admin_consent) * 10

    if total_score >= 100:
        aggregate_risk = "CRITICAL"
    elif total_score >= 60:
        aggregate_risk = "HIGH"
    elif total_score >= 30:
        aggregate_risk = "MEDIUM"
    else:
        aggregate_risk = "LOW"

    return {
        "total_score": total_score,
        "aggregate_risk": aggregate_risk,
        "scope_count": len(app_permissions),
        "critical_scopes": len([s for s in classified_scopes if s["risk_level"] == "critical"]),
        "high_scopes": len([s for s in classified_scopes if s["risk_level"] == "high"]),
        "classified_scopes": classified_scopes
    }

步骤 3:识别过度授权的应用

检测请求权限超出功能需求的应用:

"""
过度授权检测
识别 OAuth 范围相对于其功能而言过度的应用。
"""

def detect_over_permissions(inventory, approved_apps_catalog):
    """
    将实际权限与已批准的范围目录进行比对,
    找出过度授权的应用。
    """
    findings = []

    # 按应用分组权限
    app_permissions = defaultdict(list)
    for perm in inventory:
        app_permissions[perm["app_name"]].append(perm)

    for app_name, permissions in app_permissions.items():
        # 与已批准目录进行比对
        approved = approved_apps_catalog.get(app_name)

        if not approved:
            # 未知/未批准的应用
            findings.append({
                "app_name": app_name,
                "finding_type": "UNAPPROVED_APPLICATION",
                "severity": "HIGH",
                "detail": f"应用不在已批准目录中,共有 {len(permissions)} 个权限授予",
                "scopes": [p["scope"] for p in permissions],
                "recommendation": "审查并批准或撤销所有权限"
            })
            continue

        approved_scopes = set(approved.get("approved_scopes", []))
        actual_scopes = set(p["scope"] for p in permissions)

        # 找出过多范围(已授予但未批准)
        excessive = actual_scopes - approved_scopes
        if excessive:
            risk = analyze_app_risk([p for p in permissions if p["scope"] in excessive])
            findings.append({
                "app_name": app_name,
                "finding_type": "EXCESSIVE_SCOPES",
                "severity": risk["aggregate_risk"],
                "detail": f"{len(excessive)} 个超出已批准列表的范围",
                "excessive_scopes": list(excessive),
                "approved_scopes": list(approved_scopes),
                "recommendation": "移除过多范围或更新已批准目录"
            })

        # 找出未使用的范围(已批准但活动日志显示无 API 调用)
        # 这需要与 API 活动日志进行关联
        unused = approved_scopes - actual_scopes
        if unused:
            findings.append({
                "app_name": app_name,
                "finding_type": "UNUSED_APPROVED_SCOPES",
                "severity": "LOW",
                "detail": f"{len(unused)} 个已批准范围当前未授予",
                "unused_scopes": list(unused)
            })

        # 检查过于宽泛的权限
        broad_patterns = [
            ("Mail.ReadWrite", "Mail.Read", "仅需读取权限时却授予了邮件写入权限"),
            ("Files.ReadWrite.All", "Files.Read.All", "仅需读取权限时却授予了所有文件写入权限"),
            ("Directory.ReadWrite.All", "Directory.Read.All", "仅需读取权限时却授予了目录写入权限"),
            ("User.ReadWrite.All", "User.Read.All", "仅需读取权限时却授予了用户写入权限"),
        ]

        for broad, narrow, description in broad_patterns:
            if broad in actual_scopes:
                findings.append({
                    "app_name": app_name,
                    "finding_type": "OVERLY_BROAD_SCOPE",
                    "severity": "MEDIUM",
                    "detail": description,
                    "current_scope": broad,
                    "recommended_scope": narrow,
                    "recommendation": f"从 {broad} 降级为 {narrow}"
                })

    return findings

步骤 4:审计令牌使用情况并检测过期授权

识别不再活跃使用的 OAuth 令牌:

"""
令牌使用情况审计
分析登录日志和 API 活动,识别过期的 OAuth 授权。
"""

def audit_token_usage(auditor, days_inactive=90):
    """识别近期无 API 活动的 OAuth 授权。"""
    # 获取服务主体的登录活动
    url = f"{auditor.base_url}/auditLogs/signIns"
    params = {
        "$filter": f"createdDateTime ge {(datetime.utcnow() - timedelta(days=days_inactive)).isoformat()}Z and signInEventTypes/any(t: t eq 'servicePrincipal')",
        "$top": 999
    }

    active_apps = set()
    while url:
        response = requests.get(url, headers=auditor.headers, params=params)
        data = response.json()
        for signin in data.get("value", []):
            active_apps.add(signin.get("appId"))
        url = data.get("@odata.nextLink")
        params = {}

    # 与所有已授权应用进行比对
    all_grants = auditor.get_oauth2_permission_grants()
    sp_map = {sp["id"]: sp for sp in auditor.get_all_service_principals()}

    stale_grants = []
    for grant in all_grants:
        sp = sp_map.get(grant["clientId"], {})
        app_id = sp.get("appId")

        if app_id and app_id not in active_apps:
            stale_grants.append({
                "app_name": sp.get("displayName", "Unknown"),
                "app_id": app_id,
                "scopes": grant.get("scope", "").split(),
                "consent_type": grant.get("consentType"),
                "is_third_party": sp.get("appOwnerOrganizationId") != auditor.tenant_id,
                "days_inactive": days_inactive,
                "recommendation": "撤销 - {days_inactive} 天内无 API 活动"
            })

    return sorted(stale_grants, key=lambda x: len(x["scopes"]), reverse=True)

步骤 5:生成修复计划并执行范围缩减

创建并执行范围最小化修复计划:

"""
OAuth 范围修复
生成并执行范围缩减操作。
"""

def generate_remediation_plan(findings, stale_grants):
    """创建优先级排序的修复计划。"""
    plan = []

    # 优先级 1:撤销未批准的应用
    for f in findings:
        if f["finding_type"] == "UNAPPROVED_APPLICATION":
            plan.append({
                "priority": 1,
                "action": "REVOKE_ALL_PERMISSIONS",
                "app_name": f["app_name"],
                "reason": "未批准的第三方应用",
                "impact": f"移除 {len(f['scopes'])} 个权限授予",
                "risk_if_not_addressed": "CRITICAL"
            })

    # 优先级 2:移除已批准应用的过多范围
    for f in findings:
        if f["finding_type"] == "EXCESSIVE_SCOPES":
            plan.append({
                "priority": 2,
                "action": "REMOVE_EXCESSIVE_SCOPES",
                "app_name": f["app_name"],
                "scopes_to_remove": f["excessive_scopes"],
                "reason": "超出已批准目录的范围",
                "risk_if_not_addressed": f["severity"]
            })

    # 优先级 3:降级过于宽泛的范围
    for f in findings:
        if f["finding_type"] == "OVERLY_BROAD_SCOPE":
            plan.append({
                "priority": 3,
                "action": "DOWNGRADE_SCOPE",
                "app_name": f["app_name"],
                "current_scope": f["current_scope"],
                "target_scope": f["recommended_scope"],
                "reason": f["detail"],
                "risk_if_not_addressed": "MEDIUM"
            })

    # 优先级 4:撤销过期授权
    for grant in stale_grants:
        plan.append({
            "priority": 4,
            "action": "REVOKE_STALE_GRANT",
            "app_name": grant["app_name"],
            "scopes_to_revoke": grant["scopes"],
            "reason": f"{grant['days_inactive']} 天内无 API 活动",
            "risk_if_not_addressed": "MEDIUM"
        })

    return sorted(plan, key=lambda x: x["priority"])

def execute_scope_reduction(auditor, grant_id, scopes_to_remove):
    """从 OAuth 权限授予中移除特定范围。"""
    # 获取当前授权
    url = f"{auditor.base_url}/oauth2PermissionGrants/{grant_id}"
    response = requests.get(url, headers=auditor.headers)
    current_grant = response.json()

    current_scopes = set(current_grant.get("scope", "").split())
    updated_scopes = current_scopes - set(scopes_to_remove)

    if not updated_scopes:
        # 移除整个授权
        requests.delete(url, headers=auditor.headers)
        return {"action": "grant_deleted", "grant_id": grant_id}
    else:
        # 使用缩减后的范围进行更新
        update_body = {"scope": " ".join(updated_scopes)}
        requests.patch(url, headers=auditor.headers, json=update_body)
        return {
            "action": "scopes_reduced",
            "grant_id": grant_id,
            "removed": list(scopes_to_remove),
            "remaining": list(updated_scopes)
        }

核心概念

术语定义
OAuth 范围定义授予客户端应用的特定 API 访问级别的权限字符串(例如:Mail.Read、Files.ReadWrite.All)
委托权限代表已登录用户行使的 OAuth 范围,受应用权限和用户自身访问权的双重限制
应用权限无需用户上下文直接授予应用的 OAuth 范围,可访问所有用户数据(高风险)
管理员同意由管理员做出的租户级权限授予,无需个人同意即可应用于所有用户
范围最小化将 OAuth 权限减少到应用功能所需最小集合的安全原则
过期授权保持活跃但近期无 API 使用记录的 OAuth 权限,表明集成已废弃或过时

工具与系统

  • Microsoft Entra 管理中心:用于审查企业应用、同意权限和 OAuth 授权管理的门户
  • Nudge Security:用于发现 OAuth 授权、评估第三方风险并自动化范围审查的 SaaS 安全平台
  • Cerby:用于审计 OAuth 集成和管理共享账户的非 SSO 应用管理平台
  • Microsoft Graph API:用于大规模枚举和修改 OAuth 权限授予的编程接口

常见场景

场景:安全事件后的 OAuth 范围审计

背景:一次钓鱼攻击导致管理员账户被入侵,调查发现攻击者注册了一个带有 Mail.ReadWrite 和 Files.ReadWrite.All 范围的恶意 OAuth 应用,窃取了 6 个月的电子邮件。组织需要进行全面的 OAuth 范围审查。

处理方法:

  1. 立即撤销被入侵管理员会话的所有 OAuth 授权
  2. 枚举租户内所有服务主体和权限授予
  3. 标记过去 90 天内注册的所有应用进行手动审查
  4. 使用风险框架对所有第三方应用范围进行分类
  5. 识别具有严重范围的应用(Mail.ReadWrite、Files.ReadWrite.All、Directory.ReadWrite.All)
  6. 与 IT 采购的已批准应用目录进行交叉对比
  7. 立即撤销所有未批准应用的权限
  8. 将过度授权的已批准应用降级为所需最小范围
  9. 实施管理员同意工作流,防止未来出现不受控的 OAuth 授权
  10. 启用同意策略,要求对高风险范围进行管理员审批

注意事项:

  • 未经协调撤销业务关键集成的权限会导致服务中断
  • 未检查应用级权限(vs 委托权限),这类权限风险更高且常被忽视
  • 遗漏多租户应用,即发布者租户与使用租户不同的情况
  • 修复后未实施持续监控,导致无法检测新的未授权 OAuth 授权

输出格式

OAUTH 范围最小化审查报告
=========================================
租户:              corp.onmicrosoft.com
审查周期:          2026-02-01 至 2026-02-24
应用总数:          147
第三方应用:        98
第一方应用:        49

权限清单
OAuth 授权总数:        487
  委托权限:            312
  应用权限:            175
  管理员同意:          89
  用户同意:            223

风险分类
严重风险应用:     7
  - UnknownCRMApp (Mail.ReadWrite, Files.ReadWrite.All - 未批准)
  - LegacySync (Directory.ReadWrite.All - 过多权限)
  - DevToolX (Application.ReadWrite.All - 权限过于宽泛)
高风险应用:         18
中风险应用:         34
低风险应用:         88

发现问题
未批准应用:              12(立即撤销)
过多范围:                23 个应用的范围超出已批准列表
过于宽泛的权限:          15 个应用可降级处理
过期授权(90天以上):    31 个应用近期无 API 活动

修复计划
优先级 1(立即):    12 个未批准应用撤销
优先级 2(本周):    23 个过多范围移除
优先级 3(本月):    15 个范围降级
优先级 4(下季度):  31 个过期授权撤销

预计范围缩减:  总权限的 34%
Skills enregistrées