AID-E-001.004: Delegated Grant & Connected-App Authorization Revocation
Pillars: app, infra
Lifecycle: response
Scope boundary
Owns incident-time revocation of delegated OAuth grants, connected-app consents, app-role assignments, token-exchange or on-behalf-of authorizations, and SaaS application grants that can mint fresh tokens or sustain access. It does not own credential, token, principal, application-state, or pre-incident authorization design.
AID-E-001.001: AID-E-001.004 removes delegated authorization objects; AID-E-001.001 evicts root and long-lived credentials. Grant revocation does not rotate an exposed password, key, client secret, signing key, or certificate, and credential rotation does not necessarily remove an independent connected-app consent.
AID-E-001.002: AID-E-001.004 removes authorization that can sustain or recreate access; AID-E-001.002 revokes tokens and authentication sessions already issued under that authority. Both controls are required when issued artifacts remain valid after the underlying grant is removed.
AID-E-001.003: AID-E-001.004 revokes delegated grants and app authorizations; AID-E-001.003 disables the compromised AI-agent or workload principal and stops new principal issuance. Removing one grant does not disable the principal's other authorization paths, while principal disablement does not prove removal of every connected-app or delegated grant object.
AID-H-034.002: AID-E-001.004 performs incident-time removal of delegated grant objects; AID-H-034.002 enforces MCP server OAuth resource boundaries and delegation safety before compromise. Preventive OAuth design does not revoke a compromised live grant, and grant eviction does not replace correct protected-resource and audience enforcement.
AID-E-005: AID-E-001.004 removes authorization grants; AID-E-005 deletes rogue webhook, tool-registration, queue, schedule, and durable application-state records. Revoke the grant before or alongside teardown, but do not treat removal of either population as proof that the other is gone.
Implementation guidance
- AID-E-001.004-G001: Enumerate delegated OAuth grants, connected apps, app-role assignments, and SaaS consents tied to the compromised principal or agent.
- AID-E-001.004-G002: Revoke delegated OAuth grants, connected-app consents, token-exchange or on-behalf-of authorizations, and app-role assignments that allow continued access after compromise.
- AID-E-001.004-G003: Verify grant-revocation propagation and monitor for attempted reuse of revoked connected-app access.
AID-E-001.004-G001
Take a read-only, class-complete inventory before revocation
Inventory is a prerequisite, not proof of eviction. Use read-only Graph permissions and enumerate each declared authorization class separately. The Entra example below exports delegated OAuth grants and direct user app-role assignments. Microsoft Graph also returns assignments inherited through groups, so the adapter filters direct assignments by immutable user principal ID; group-derived authorization is a separate class that needs its own signed scope and group-membership adapter. Connected-app, OBO/token-exchange, cross-application, and SaaS-native authorization objects require provider-specific read-only adapters; list those classes in the signed incident scope and fail the inventory when an applicable class has no authoritative enumerator. A workload identity or service principal is not a user: select its provider endpoint explicitly rather than silently returning an empty user inventory.
Export Entra delegated grants and app-role assignments
# File: incident_response/export_entra_principal_authorizations.ps1
param(
[Parameter(Mandatory=$true)][string]$UserPrincipalName,
[Parameter(Mandatory=$true)][string]$IncidentId,
[Parameter(Mandatory=$true)][string]$OutputPath
)
$ErrorActionPreference = "Stop"
Connect-MgGraph -Scopes "Directory.Read.All" -NoWelcome
$user = Get-MgUser -UserId $UserPrincipalName -Property Id,UserPrincipalName
$delegated = @(
Get-MgUserOauth2PermissionGrant -UserId $user.Id -All |
Sort-Object Id |
ForEach-Object {
[ordered]@{
grant_id = $_.Id
client_id = $_.ClientId
resource_id = $_.ResourceId
scope = $_.Scope
consent_type = $_.ConsentType
principal_id = $_.PrincipalId
}
}
)
$appRoles = @(
Get-MgUserAppRoleAssignment -UserId $user.Id -All |
Where-Object { $_.PrincipalId -eq $user.Id } |
Sort-Object Id |
ForEach-Object {
[ordered]@{
assignment_id = $_.Id
app_role_id = $_.AppRoleId
resource_id = $_.ResourceId
principal_id = $_.PrincipalId
created_at = $_.CreatedDateTime
}
}
)
$record = [ordered]@{
schema_version = "aidefend.e001004.authorization-inventory.v1"
control = "AID-E-001.004"
provider = "microsoft_entra"
incident_id = $IncidentId
principal_id = $user.Id
principal_type = "user"
user_principal_name = $user.UserPrincipalName
enumerated_classes = @("delegated_oauth_grant", "user_app_role_assignment")
delegated_grants = $delegated
app_role_assignments = $appRoles
exported_at = (Get-Date).ToUniversalTime().ToString("o")
}
$json = $record | ConvertTo-Json -Depth 10 -Compress
$parent = Split-Path -Parent $OutputPath
if ($parent) {
New-Item -ItemType Directory -Force -Path $parent | Out-Null
}
[System.IO.File]::WriteAllText(
$OutputPath,
$json + [Environment]::NewLine,
[System.Text.UTF8Encoding]::new($false)
)
#!/usr/bin/env bash
set -euo pipefail
: "${ENTRA_USER_PRINCIPAL_NAME:?Set ENTRA_USER_PRINCIPAL_NAME}"
: "${INCIDENT_ID:?Set INCIDENT_ID from the verified incident record}"
: "${INVENTORY_PHASE:?Set INVENTORY_PHASE to before or after}"
: "${INVENTORY_SIGNING_KEY:?Set INVENTORY_SIGNING_KEY for Cosign}"
: "${E001004_COMMAND_TIMEOUT_SECONDS:?Set the signed runtime-profile timeout}"
if [[ ! "$INCIDENT_ID" =~ ^INC-[A-Z0-9][A-Z0-9._-]{2,63}$ ]]; then
echo "INCIDENT_ID is not a bounded incident identifier" >&2
exit 2
fi
case "$INVENTORY_PHASE" in before|after) ;; *) echo "INVENTORY_PHASE must be before or after" >&2; exit 2;; esac
if [[ ! "$E001004_COMMAND_TIMEOUT_SECONDS" =~ ^[0-9]+([.][0-9]+)?$ ]] ||
! awk -v value="$E001004_COMMAND_TIMEOUT_SECONDS" 'BEGIN { exit !(value > 0) }'; then
echo "command timeout must be positive" >&2
exit 2
fi
umask 077
OUTPUT_DIR="artifacts/$INCIDENT_ID"
OUTPUT_PATH="$OUTPUT_DIR/entra-authorization-$INVENTORY_PHASE.json"
BUNDLE_PATH="$OUTPUT_PATH.sigstore.json"
mkdir -p -- "$OUTPUT_DIR"
pwsh -File incident_response/export_entra_principal_authorizations.ps1 \
-UserPrincipalName "$ENTRA_USER_PRINCIPAL_NAME" \
-IncidentId "$INCIDENT_ID" \
-OutputPath "$OUTPUT_PATH"
cosign sign-blob --yes --key env://INVENTORY_SIGNING_KEY \
--bundle "$BUNDLE_PATH" "$OUTPUT_PATH"
timeout "$E001004_COMMAND_TIMEOUT_SECONDS" \
cosign verify-blob --key trust/inventory-reader.pub \
--bundle "$BUNDLE_PATH" "$OUTPUT_PATH"
Verify completeness before any destructive action
A separate incident reviewer compares enumerated_classes with the signed provider-class scope, checks that the principal type and immutable provider ID are correct, verifies the incident-scoped inventory payload and sibling Cosign bundle, and records the exact target object IDs in a different signed revocation manifest. Empty results are acceptable only when the authoritative endpoint successfully enumerated an applicable class and the signed scope expected zero objects. Missing permissions, pagination failure, unsupported principal type, or an undeclared provider authorization class is insufficient data and blocks revocation acceptance.
Action: run this exporter with INVENTORY_PHASE=before before revocation and INVENTORY_PHASE=after after propagation, then preserve both signed inventories and provider readback metadata before removing any grant or assignment. Never request write scopes merely to enumerate.
AID-E-001.004-G002
Production implementation
A provider adapter selected from trusted configuration re-fetches each object and compares all signed fields. It invokes the conditional delete or revoke API for that exact grant or consent object and removes exact app-role or connected-app assignments. Already-issued access tokens, refresh tokens, and issuer or gateway sessions are enumerated and invalidated under AID-E-001.002; those receipts are dependency evidence, not E-001.004 evidence. Unsupported class or partial provider failure aborts acceptance and records per-object state; it never broad-revokes unlisted principals/applications.
Independent verification
A separate read-only connector re-enumerates all declared grant classes at a newer consistent revision and proves authorized objects absent/disabled and unlisted controls unchanged. Controlled OAuth authorization, OBO or token-exchange, fresh-token issuance, and app-role attempts for applicable revoked paths must be denied; a clean approved application control still succeeds.
AID-E-001.004-G003
Verify only the signed target population
Do not interpret the entire pre-incident inventory as malicious. Before revocation, an authorized reviewer signs the exact delegated-grant and app-role-assignment IDs to remove. Verification must prove that every target disappeared, every non-target object is unchanged, no new object appeared during the action window, and the incident and immutable principal bindings match. This avoids both false PASS and collateral revocation.
{
"schema_version": "aidefend.e001004.revocation-targets.v1",
"control": "AID-E-001.004",
"provider": "microsoft_entra",
"incident_id": "INC-2026-0718-0042",
"principal_id": "51d3c809-58a2-46b8-8136-e3fda2bfef85",
"authorization_id": "change://CHG-2026-0718-031",
"before_inventory_sha256": "c61e73c8f22a997daf0f9a4acbb7e0c45bc7184a2b81cb184034a3e879b330e2",
"target_delegated_grant_ids": ["df8f12d6-3f82-4a20-8d31-7105a8d65d4f"],
"target_app_role_assignment_ids": ["602dd83c-4cec-4a54-9e11-52f1ae6c64a8"]
}#!/usr/bin/env bash
set -euo pipefail
: "${INCIDENT_ID:?Set INCIDENT_ID from the verified incident record}"
: "${E001004_COMMAND_TIMEOUT_SECONDS:?Set the signed runtime-profile timeout}"
if [[ ! "$INCIDENT_ID" =~ ^INC-[A-Z0-9][A-Z0-9._-]{2,63}$ ]]; then
echo "INCIDENT_ID is not a bounded incident identifier" >&2
exit 2
fi
if [[ ! "$E001004_COMMAND_TIMEOUT_SECONDS" =~ ^[0-9]+([.][0-9]+)?$ ]] ||
! awk -v value="$E001004_COMMAND_TIMEOUT_SECONDS" 'BEGIN { exit !(value > 0) }'; then
echo "command timeout must be positive" >&2
exit 2
fi
artifact_root="artifacts/$INCIDENT_ID"
SNAPSHOT_ROOT="$(mktemp -d)"
chmod 700 "$SNAPSHOT_ROOT"
trap 'rm -rf -- "$SNAPSHOT_ROOT"' EXIT
: > "$SNAPSHOT_ROOT/verified-inputs.sha256"
chmod 400 "$SNAPSHOT_ROOT/verified-inputs.sha256"
hash_file() { sha256sum -- "$1" | awk '{print $1}'; }
copy_and_verify_pair() {
local source_payload=$1 trust_key=$2 snapshot_name=$3
local source_bundle="${source_payload}.sigstore.json"
local snapshot_payload="$SNAPSHOT_ROOT/$snapshot_name"
local snapshot_bundle="$SNAPSHOT_ROOT/$snapshot_name.sigstore.json"
if [[ ! -s "$source_payload" || ! -s "$source_bundle" ]]; then
echo "missing or empty payload/bundle pair: $source_payload" >&2
return 1
fi
install -m 0400 -- "$source_payload" "$snapshot_payload"
install -m 0400 -- "$source_bundle" "$snapshot_bundle"
local payload_sha bundle_sha
payload_sha=$(hash_file "$snapshot_payload")
bundle_sha=$(hash_file "$snapshot_bundle")
timeout "$E001004_COMMAND_TIMEOUT_SECONDS" \
cosign verify-blob --key "$trust_key" \
--bundle "$snapshot_bundle" "$snapshot_payload"
if [[ $(hash_file "$snapshot_payload") != "$payload_sha" ||
$(hash_file "$snapshot_bundle") != "$bundle_sha" ]]; then
echo "verified payload or bundle snapshot changed: $snapshot_name" >&2
return 1
fi
chmod 600 "$SNAPSHOT_ROOT/verified-inputs.sha256"
printf '%s %s\n%s %s\n' \
"$payload_sha" "$snapshot_name" \
"$bundle_sha" "$snapshot_name.sigstore.json" \
>> "$SNAPSHOT_ROOT/verified-inputs.sha256"
chmod 400 "$SNAPSHOT_ROOT/verified-inputs.sha256"
}
copy_and_verify_pair \
"$artifact_root/revocation-targets.json" \
trust/revocation-authority.pub revocation-targets.json
copy_and_verify_pair \
"$artifact_root/entra-authorization-before.json" \
trust/inventory-reader.pub entra-authorization-before.json
copy_and_verify_pair \
"$artifact_root/entra-authorization-after.json" \
trust/inventory-reader.pub entra-authorization-after.json
python incident_response/verify_authorization_revocation.py \
--before "$SNAPSHOT_ROOT/entra-authorization-before.json" \
--targets "$SNAPSHOT_ROOT/revocation-targets.json" \
--after "$SNAPSHOT_ROOT/entra-authorization-after.json" \
--output "$artifact_root/revocation-state-verification.json"
Compare exact before, target, and after sets
# File: incident_response/verify_authorization_revocation.py
from __future__ import annotations
import argparse
import hashlib
import json
from datetime import datetime
from pathlib import Path
from typing import Any
INVENTORY_FIELDS = {
"schema_version",
"control",
"provider",
"incident_id",
"principal_id",
"principal_type",
"user_principal_name",
"enumerated_classes",
"delegated_grants",
"app_role_assignments",
"exported_at",
}
TARGET_FIELDS = {
"schema_version",
"control",
"provider",
"incident_id",
"principal_id",
"authorization_id",
"before_inventory_sha256",
"target_delegated_grant_ids",
"target_app_role_assignment_ids",
}
DELEGATED_FIELDS = {
"grant_id",
"client_id",
"resource_id",
"scope",
"consent_type",
"principal_id",
}
APP_ROLE_FIELDS = {
"assignment_id",
"app_role_id",
"resource_id",
"principal_id",
"created_at",
}
EXPECTED_CLASSES = {
"delegated_oauth_grant",
"user_app_role_assignment",
}
def reject_duplicate_keys(pairs):
value = {}
for key, item in pairs:
if key in value:
raise ValueError(f"duplicate signed JSON key: {key}")
value[key] = item
return value
def reject_nonfinite(value):
raise ValueError(f"non-finite signed JSON value: {value}")
def load(path: Path) -> tuple[dict[str, Any], bytes]:
raw = path.read_bytes()
if not raw:
raise ValueError(f"{path} is empty")
try:
value = json.loads(
raw.decode("utf-8", errors="strict"),
object_pairs_hook=reject_duplicate_keys,
parse_constant=reject_nonfinite,
)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ValueError(f"{path} is not strict UTF-8 JSON") from exc
if not isinstance(value, dict):
raise ValueError(f"{path} must contain one JSON object")
return value, raw
def sha256_bytes(raw: bytes) -> str:
return hashlib.sha256(raw).hexdigest()
def require_sha256(value: object, field: str) -> str:
if (
not isinstance(value, str)
or len(value) != 64
or set(value) - set("0123456789abcdef")
or value == "0" * 64
):
raise ValueError(f"{field} must be a nonzero lowercase SHA-256 digest")
return value
def exact_target_ids(value: object, field: str) -> set[str]:
if (
not isinstance(value, list)
or value != sorted(set(value))
or any(
not isinstance(item, str) or not item or "*" in item
for item in value
)
):
raise ValueError(f"{field} must be a sorted unique exact-ID list")
return set(value)
def index_rows(
document: dict[str, Any],
collection: str,
id_field: str,
expected_fields: set[str],
) -> dict[str, dict[str, Any]]:
rows = document.get(collection)
if not isinstance(rows, list):
raise ValueError(f"{collection} must be a complete array")
indexed: dict[str, dict[str, Any]] = {}
for row in rows:
if not isinstance(row, dict) or set(row) != expected_fields:
raise ValueError(f"{collection} row schema differs")
row_id = row.get(id_field)
if not isinstance(row_id, str) or not row_id:
raise ValueError(f"{collection}.{id_field} is required")
if row_id in indexed:
raise ValueError(f"duplicate {collection} object {row_id}")
if row.get("principal_id") != document["principal_id"]:
raise ValueError(f"{collection} object is bound to another principal")
indexed[row_id] = row
return indexed
def parse_time(value: object, field: str) -> datetime:
if not isinstance(value, str) or not value:
raise ValueError(f"{field} is required")
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise ValueError(f"{field} must include a timezone")
return parsed
def verify(
before_path: Path,
target_path: Path,
after_path: Path,
) -> dict[str, Any]:
before, before_bytes = load(before_path)
targets, target_bytes = load(target_path)
after, after_bytes = load(after_path)
if (
set(before) != INVENTORY_FIELDS
or set(after) != INVENTORY_FIELDS
or before.get("schema_version")
!= "aidefend.e001004.authorization-inventory.v1"
or after.get("schema_version")
!= "aidefend.e001004.authorization-inventory.v1"
or set(targets) != TARGET_FIELDS
or targets.get("schema_version")
!= "aidefend.e001004.revocation-targets.v1"
):
raise ValueError("inventory or target schema differs")
authorization_id = targets.get("authorization_id")
if (
not isinstance(authorization_id, str)
or not authorization_id
or "*" in authorization_id
):
raise ValueError("authorization_id must be an exact nonempty value")
signed_before_digest = require_sha256(
targets.get("before_inventory_sha256"),
"targets.before_inventory_sha256",
)
common = ("control", "provider", "incident_id", "principal_id")
if (
before.get("control") != "AID-E-001.004"
or any(before.get(field) != after.get(field) for field in common)
or any(before.get(field) != targets.get(field) for field in common)
or before.get("principal_type") != after.get("principal_type")
or before.get("user_principal_name")
!= after.get("user_principal_name")
or set(before.get("enumerated_classes", [])) != EXPECTED_CLASSES
or set(after.get("enumerated_classes", [])) != EXPECTED_CLASSES
):
raise ValueError("incident, provider, principal, or class binding differs")
if parse_time(after["exported_at"], "after.exported_at") <= parse_time(
before["exported_at"], "before.exported_at"
):
raise ValueError("after inventory is not newer than before inventory")
if signed_before_digest != sha256_bytes(before_bytes):
raise ValueError("target manifest does not bind the before inventory")
before_delegated = index_rows(
before, "delegated_grants", "grant_id", DELEGATED_FIELDS
)
after_delegated = index_rows(
after, "delegated_grants", "grant_id", DELEGATED_FIELDS
)
before_roles = index_rows(
before, "app_role_assignments", "assignment_id", APP_ROLE_FIELDS
)
after_roles = index_rows(
after, "app_role_assignments", "assignment_id", APP_ROLE_FIELDS
)
delegated_targets = exact_target_ids(
targets["target_delegated_grant_ids"],
"target_delegated_grant_ids",
)
role_targets = exact_target_ids(
targets["target_app_role_assignment_ids"],
"target_app_role_assignment_ids",
)
if not delegated_targets and not role_targets:
raise ValueError("revocation target population is empty")
if not delegated_targets.issubset(before_delegated):
raise ValueError("delegated target was absent from the signed before set")
if not role_targets.issubset(before_roles):
raise ValueError("app-role target was absent from the signed before set")
expected_delegated = set(before_delegated) - delegated_targets
expected_roles = set(before_roles) - role_targets
if set(after_delegated) != expected_delegated:
raise RuntimeError("delegated after-set is not the exact target difference")
if set(after_roles) != expected_roles:
raise RuntimeError("app-role after-set is not the exact target difference")
if any(
after_delegated[item] != before_delegated[item]
for item in expected_delegated
):
raise RuntimeError("a non-target delegated grant changed")
if any(after_roles[item] != before_roles[item] for item in expected_roles):
raise RuntimeError("a non-target app-role assignment changed")
return {
"schema_version":
"aidefend.e001004.revocation-state-verification.v1",
"control": "AID-E-001.004",
"provider": before["provider"],
"incident_id": before["incident_id"],
"principal_id": before["principal_id"],
"authorization_id": targets["authorization_id"],
"before_inventory_sha256": sha256_bytes(before_bytes),
"target_manifest_sha256": sha256_bytes(target_bytes),
"after_inventory_sha256": sha256_bytes(after_bytes),
"verified_revoked_delegated_grant_ids":
sorted(delegated_targets),
"verified_revoked_app_role_assignment_ids":
sorted(role_targets),
"verified_unchanged_delegated_grant_ids":
sorted(expected_delegated),
"verified_unchanged_app_role_assignment_ids":
sorted(expected_roles),
"authoritative_state_result": "PASS",
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--before", type=Path, required=True)
parser.add_argument("--targets", type=Path, required=True)
parser.add_argument("--after", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
result = verify(args.before, args.targets, args.after)
with args.output.open("x", encoding="utf-8", newline="\n") as handle:
json.dump(result, handle, sort_keys=True, separators=(",", ":"))
handle.write("\n")
if __name__ == "__main__":
main()
Complete closure with provider behavior, not inventory alone
Run the after-inventory and verifier under a separately credentialed read-only identity after provider propagation. For each targeted path, run a provider-approved negative issuance or access probe: new delegated authorization, OBO or token-exchange issuance, and app-role protected-resource access must be denied according to the signed probe contract. Run an unrevoked clean control to distinguish correct denial from provider outage. Sign and retain the authoritative-state result, probe request digests, provider decision and correlation IDs, clean-control result, and timestamps. The set comparison is only state proof; E-001.004 closes after the applicable negative probes also pass.
Reuse monitoring is a Detect handoff. Activate a versioned rule keyed to the exact revoked principal, client, resource, grant, and assignment identifiers without storing reusable tokens. A monitoring rule or absence of alerts never substitutes for revocation readback.
Action: require exact target absence, exact non-target preservation, provider negative probes, and a clean positive control. Any new object, collateral deletion, stale inventory, issuance success, or provider outage blocks PASS.
Open-source tools
- Microsoft Graph PowerShell SDK
- GAM7
- Okta Terraform Provider
- jq
Commercial / hosted tools
- Microsoft Entra ID
- Okta API
- Okta Cross App Access
- Google Workspace Admin console
- Google Admin SDK
- AppOmni SaaS Security Platform
- CrowdStrike Falcon Shield
- Obsidian Security
Threat mappings
MITRE ATLAS
- AML.T0012 Valid Accounts (revoking the delegated authorization prevents fresh delegated access or issuance; AID-E-001.002 remains required for already-issued tokens)
- AML.T0121 AI Agent Environment Reconstruction (revoked delegated grants cannot mint fresh tokens for environment reconstruction)
MAESTRO
- Agent Identity Attack (L7)
- Privilege Escalation (Cross-Layer)
- Lateral Movement (Cross-Layer)
OWASP LLM Top 10 2026
- LLM03:2026 Excessive Agency (grant revocation removes delegated authority after compromise)
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI03:2026 Identity and Privilege Abuse
- ASI02:2026 Tool Misuse and Exploitation (revoking the compromised delegated grant removes the connected app or tool authority used for misuse)
NIST Adversarial Machine Learning 2025
- NISTAML.039 Compromising connected resources
Cisco Integrated AI Security and Safety Framework
- AISubtech-14.1.2 Insufficient Access Controls
- AISubtech-14.2.1 Permission Escalation via Delegation
- AISubtech-2.1.2 Capability and Permission Overreach (removes the compromised agent delegation path; other grants and issued tokens remain separate)
- AITech-14.2 Abuse of Delegated Authority
- AITech-2.1 Excessive Agency (removes the compromised agent delegation path; other grants and issued tokens remain separate)
- AITech-8.2 Data Exfiltration / Exposure (stops continued disclosure through the revoked connected-app authorization path)
Google Secure AI Framework 2.0 - Risks
- RA: Rogue Actions (revoking delegated grants stops unauthorized connected-app actions)
- SDD: Sensitive Data Disclosure (grant revocation stops ongoing data access)
- MXF: Model Exfiltration (grant revocation stops model/data access through connected apps)
Databricks AI Security Framework 3.0
- Platform 12.4: Unauthorized privileged access
- Agents - Core 13.3: Privilege Compromise
- Agents - Tools MCP Client 13.31: Excessive Permission Granting