mcpField
P
performing-oauth-scope-minimization-review

performing-oauth-scope-minimization-review

github.com/mukul975セキュリティ35.2k129 回インストール8 週間前に更新
リポジトリ

performing-oauth-scope-minimization-review について

サードパーティーの OAuth 権限を点検する、アイデンティティおよびセキュリティチーム向けのスキルです。権限のリスク、アプリの必要性、利用状況から過剰・未使用の権限を特定し、縮小計画の作成を支援します。

主な機能

  1. 01OAuth アプリと権限の棚卸し
  2. 02スコープのリスク分類
  3. 03過剰な権限を持つアプリの特定
  4. 04トークン利用と未使用権限の監査
  5. 05スコープ縮小計画の作成

活用シーン

  1. 01定期的なサードパーティー権限の点検
  2. 02インシデント後の OAuth 権限監査
  3. 03コンプライアンス向けアクセス記録
  4. 04アプリ移行時の権限整理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill performing-oauth-scope-minimization-review

インストール後、Claude Code で/performing-oauth-scope-minimization-reviewを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

LICENSE
references/
api-reference.md
scripts/
agent.py

詳細

OAuth スコープの最小化レビューの実施

使用するタイミング

  • サードパーティアプリケーションの OAuth 権限を年次または四半期ごとにレビューするとき
  • OAuth トークンの侵害や不正なデータアクセスを伴うセキュリティインシデントの発生後
  • サードパーティによるデータアクセスの記録が必要なコンプライアンス監査(GDPR 第 28 条、SOC 2)
  • OAuth の許可を通じて組織のデータにアクセスするシャドー IT アプリケーションが見つかったとき
  • 権限の整理が必要な SaaS アプリケーションの移行または統合
  • API 統合に最小権限の原則を適用するとき

使用しないでください。同じ信頼境界内のファーストパーティアプリケーションの権限をレビューする場合。OAuth スコープの最小化は、サードパーティおよび境界をまたぐ同意の許可を対象とします。

前提条件

  • ID プロバイダー(Microsoft Entra ID、Okta、Google Workspace)の管理者アクセス権
  • Microsoft Graph API のアクセス許可:Application.Read.All、OAuth2PermissionGrant.ReadWrite.All
  • 調達部門または IT ガバナンス部門が管理する、承認済みサードパーティ統合の一覧
  • OAuth スコープのリスク分類フレームワーク
  • トークン分析用ツール(手動レビュー用の jwt.io、一括分析用の自動スクリプト)

ワークフロー

ステップ 1:すべての OAuth 許可と同意済み権限を一覧化する

すべての OAuth アプリケーション登録と委任されたアクセス許可を列挙します。

"""
OAuth Grant Inventory - Microsoft Entra ID
Enumerates all application registrations, service principals,
and delegated/application permission grants.
"""
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):
        """Get all service principals (enterprise applications)."""
        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):
        """Get all delegated permission grants (user consent)."""
        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):
        """Get application permission assignments for a service principal."""
        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):
        """Build comprehensive OAuth permission inventory."""
        service_principals = self.get_all_service_principals()
        delegated_grants = self.get_oauth2_permission_grants()

        # Map service principal IDs to names
        sp_map = {sp["id"]: sp for sp in service_principals}

        inventory = []

        # Process delegated permissions
        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 or Principal
                    "principal_id": grant.get("principalId"),
                    "granted_date": sp.get("createdDateTime"),
                    "is_enabled": sp.get("accountEnabled", True)
                })

        # Process application permissions
        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 Scope Risk Classification
Maps API scopes to risk levels based on data sensitivity and access breadth.
"""

MICROSOFT_GRAPH_SCOPE_RISK = {
    # CRITICAL - Full administrative or unrestricted access
    "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": "Can read/write all data, modify directory, or impersonate users",
        "review_frequency": "Monthly",
        "requires_admin_consent": True
    },
    # HIGH - Broad read access or sensitive data write
    "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": "Broad read access to sensitive organizational data",
        "review_frequency": "Quarterly",
        "requires_admin_consent": True
    },
    # MEDIUM - Scoped data access
    "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": "Scoped access to specific data types with write capability",
        "review_frequency": "Semi-annually"
    },
    # LOW - Minimal or user-profile only access
    "low": {
        "scopes": [
            "User.Read",
            "openid",
            "profile",
            "email",
            "offline_access",
            "Calendars.Read.Shared",
            "People.Read",
            "User.ReadBasic.All",
        ],
        "risk_description": "Basic user profile or minimal scoped access",
        "review_frequency": "Annually"
    }
}

def classify_scope_risk(scope):
    """Classify a single OAuth scope by risk level."""
    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"]
            }
    # Unknown scopes default to HIGH risk
    return {
        "scope": scope,
        "risk_level": "high",
        "description": "Unknown scope - requires manual classification",
        "review_frequency": "Quarterly"
    }

def analyze_app_risk(app_permissions):
    """Calculate aggregate risk score for an application's 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)

    # Bonus risk for application (vs delegated) permissions
    app_type_permissions = [p for p in app_permissions if p["permission_type"] == "Application"]
    total_score += len(app_type_permissions) * 15

    # Bonus risk for admin-consented broad access
    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:過剰な権限を持つアプリケーションを特定する

機能上必要な範囲を超えるアクセス許可を要求しているアプリを検出します。

"""
Over-Permission Detection
Identifies applications with excessive OAuth scopes relative to their function.
"""

def detect_over_permissions(inventory, approved_apps_catalog):
    """
    Compare actual permissions against approved scope catalog
    to find over-permissioned applications.
    """
    findings = []

    # Group permissions by application
    app_permissions = defaultdict(list)
    for perm in inventory:
        app_permissions[perm["app_name"]].append(perm)

    for app_name, permissions in app_permissions.items():
        # Check against approved catalog
        approved = approved_apps_catalog.get(app_name)

        if not approved:
            # Unknown/unapproved application
            findings.append({
                "app_name": app_name,
                "finding_type": "UNAPPROVED_APPLICATION",
                "severity": "HIGH",
                "detail": f"Application not in approved catalog with {len(permissions)} permission grants",
                "scopes": [p["scope"] for p in permissions],
                "recommendation": "Review and approve or revoke all permissions"
            })
            continue

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

        # Find excessive scopes (granted but not approved)
        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)} scopes beyond approved list",
                "excessive_scopes": list(excessive),
                "approved_scopes": list(approved_scopes),
                "recommendation": "Remove excessive scopes or update approved catalog"
            })

        # Find unused scopes (approved but activity logs show no API calls)
        # This requires API activity log correlation
        unused = approved_scopes - actual_scopes
        if unused:
            findings.append({
                "app_name": app_name,
                "finding_type": "UNUSED_APPROVED_SCOPES",
                "severity": "LOW",
                "detail": f"{len(unused)} approved scopes not currently granted",
                "unused_scopes": list(unused)
            })

        # Check for overly broad permissions
        broad_patterns = [
            ("Mail.ReadWrite", "Mail.Read", "Write access to mail when only read needed"),
            ("Files.ReadWrite.All", "Files.Read.All", "Write access to all files when only read needed"),
            ("Directory.ReadWrite.All", "Directory.Read.All", "Write access to directory when only read needed"),
            ("User.ReadWrite.All", "User.Read.All", "Write access to users when only read needed"),
        ]

        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"Downgrade from {broad} to {narrow}"
                })

    return findings
ステップ 4:トークンの使用状況を監査し、古い許可を検出する

現在は使用されていない OAuth トークンを特定します。

"""
Token Usage Audit
Analyzes sign-in logs and API activity to identify stale OAuth grants.
"""

def audit_token_usage(auditor, days_inactive=90):
    """Identify OAuth grants with no recent API activity."""
    # Get sign-in activity for service principals
    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 = {}

    # Compare against all granted apps
    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": "Revoke - no API activity in {days_inactive} days"
            })

    return sorted(stale_grants, key=lambda x: len(x["scopes"]), reverse=True)
ステップ 5:是正計画を作成し、スコープを縮小する

スコープ最小化の是正計画を作成し、実行します。

"""
OAuth Scope Remediation
Generates and executes scope reduction actions.
"""

def generate_remediation_plan(findings, stale_grants):
    """Create prioritized remediation plan."""
    plan = []

    # Priority 1: Revoke unapproved applications
    for f in findings:
        if f["finding_type"] == "UNAPPROVED_APPLICATION":
            plan.append({
                "priority": 1,
                "action": "REVOKE_ALL_PERMISSIONS",
                "app_name": f["app_name"],
                "reason": "Unapproved third-party application",
                "impact": f"Removes {len(f['scopes'])} permission grants",
                "risk_if_not_addressed": "CRITICAL"
            })

    # Priority 2: Remove excessive scopes from approved apps
    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": "Scopes beyond approved catalog",
                "risk_if_not_addressed": f["severity"]
            })

    # Priority 3: Downgrade overly broad scopes
    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"
            })

    # Priority 4: Revoke stale grants
    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"No API activity in {grant['days_inactive']} days",
            "risk_if_not_addressed": "MEDIUM"
        })

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

def execute_scope_reduction(auditor, grant_id, scopes_to_remove):
    """Remove specific scopes from an OAuth permission grant."""
    # Get current grant
    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:
        # Remove entire grant
        requests.delete(url, headers=auditor.headers)
        return {"action": "grant_deleted", "grant_id": grant_id}
    else:
        # Update with reduced scopes
        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. リスクの高いスコープには管理者の承認を求める同意ポリシーを有効にします

注意点:

  • ビジネスクリティカルな連携のアクセス許可を、調整せずに取り消すと、サービスが中断します
  • 委任されたアクセス許可よりリスクが高く、見落とされがちなアプリケーションレベルのアクセス許可を確認しないこと
  • 発行元のテナントが利用側のテナントと異なるマルチテナントアプリケーションを見落とすこと
  • 修復後に新たな未承認の OAuth グラントを検出するための継続的な監視を導入しないこと

出力形式

OAUTH SCOPE MINIMIZATION REVIEW REPORT
=========================================
Tenant:              corp.onmicrosoft.com
Review Period:       2026-02-01 to 2026-02-24
Total Applications:  147
Third-Party Apps:    98
First-Party Apps:    49

PERMISSION INVENTORY
Total OAuth Grants:          487
  Delegated Permissions:     312
  Application Permissions:   175
  Admin-Consented:           89
  User-Consented:            223

RISK CLASSIFICATION
Critical Risk Apps:     7
  - UnknownCRMApp (Mail.ReadWrite, Files.ReadWrite.All - UNAPPROVED)
  - LegacySync (Directory.ReadWrite.All - EXCESSIVE)
  - DevToolX (Application.ReadWrite.All - OVERLY BROAD)
High Risk Apps:         18
Medium Risk Apps:       34
Low Risk Apps:          88

FINDINGS
Unapproved Applications:        12 (REVOKE IMMEDIATELY)
Excessive Scopes:               23 apps with scopes beyond approved list
Overly Broad Permissions:       15 apps that can be downgraded
Stale Grants (90+ days):        31 apps with no recent API activity

REMEDIATION PLAN
Priority 1 (Immediate):    12 unapproved app revocations
Priority 2 (This Week):    23 excessive scope removals
Priority 3 (This Month):   15 scope downgrades
Priority 4 (Next Quarter): 31 stale grant revocations

Estimated Scope Reduction:  34% of total permissions

コメント