{
  "nbformat": 4,
  "nbformat_minor": 5,
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.13.0"
    },
    "blog_metadata": {
      "topic": "Zero Trust for AI Agents: The Controls Every Microsoft Shop Should Prioritize",
      "slug": "zero-trust-for-ai-agents-the-controls-every-microsoft-shop-s",
      "generated_by": "LinkedIn Post Generator + Azure OpenAI",
      "generated_at": "2026-07-09T21:04:02.136Z"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Zero Trust for AI Agents: The Controls Every Microsoft Shop Should Prioritize\n",
        "\n",
        "AI agents create a new access plane across Microsoft environments, which means governance has to start with identity, permissions, consent, device trust, data protection, and telemetry rather than model quality alone. This notebook turns the blog post into hands-on validation steps you can run in a lab, dev tenant, or with sample data to quickly assess agent-related risk."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "%pip install requests pandas python-dateutil"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import os\n",
        "import csv\n",
        "import json\n",
        "from io import StringIO\n",
        "from datetime import datetime, timezone, timedelta\n",
        "\n",
        "import requests\n",
        "import pandas as pd"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Zero Trust mental model for AI agents\n",
        "\n",
        "This diagram captures the core idea from the post: agent security is mostly identity plumbing done correctly. The key review points are token issuance, app registrations, service principals, permissions, owners, consent type, and credential hygiene."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "zero_trust_flow = {\n",
        "    'User or AI Agent': ['Conditional Access'],\n",
        "    'Conditional Access': ['Entra ID Token Issuance'],\n",
        "    'Entra ID Token Issuance': ['App Registration / Service Principal'],\n",
        "    'App Registration / Service Principal': ['Graph / M365 / Azure APIs', 'Key Vault / Managed Identity'],\n",
        "    'Governance Review': ['Permissions', 'Owners', 'Consent Type', 'Credential Hygiene'],\n",
        "    'Permissions': ['Zero Trust Decision'],\n",
        "    'Owners': ['Zero Trust Decision'],\n",
        "    'Consent Type': ['Zero Trust Decision'],\n",
        "    'Credential Hygiene': ['Zero Trust Decision'],\n",
        "}\n",
        "\n",
        "for src, dsts in zero_trust_flow.items():\n",
        "    for dst in dsts:\n",
        "        print(f'{src} -> {dst}')"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Required environment variables for Microsoft Graph inventory\n",
        "\n",
        "Set the following variable before running the next cell:\n",
        "\n",
        "- `GRAPH_TOKEN`: A Microsoft Graph bearer token with permission to read applications and owners in a test or dev tenant.\n",
        "\n",
        "If you do not want to call a live tenant, leave it unset and the code will fall back to sample data."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Inventory app registrations and summarize Graph permissions plus owners\n",
        "\n",
        "This example helps surface three immediate governance issues: apps with many Graph permissions, apps with no owners, and apps nobody remembers approving. It uses Microsoft Graph if a token is present, otherwise it runs against built-in sample data for safe validation."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import os\n",
        "import requests\n",
        "import pandas as pd\n",
        "\n",
        "GRAPH_APP_ID = '00000003-0000-0000-c000-000000000000'\n",
        "TOKEN = os.getenv('GRAPH_TOKEN', '').strip()\n",
        "\n",
        "sample_apps = [\n",
        "    {\n",
        "        'id': '1',\n",
        "        'appId': 'app-001',\n",
        "        'displayName': 'Copilot Helper',\n",
        "        'requiredResourceAccess': [\n",
        "            {\n",
        "                'resourceAppId': GRAPH_APP_ID,\n",
        "                'resourceAccess': [{'id': 'a', 'type': 'Scope'}]\n",
        "            }\n",
        "        ],\n",
        "        'owners': [{'displayName': 'Alice Admin', 'userPrincipalName': 'alice@contoso.com'}],\n",
        "    },\n",
        "    {\n",
        "        'id': '2',\n",
        "        'appId': 'app-002',\n",
        "        'displayName': 'Sales Agent',\n",
        "        'requiredResourceAccess': [\n",
        "            {\n",
        "                'resourceAppId': GRAPH_APP_ID,\n",
        "                'resourceAccess': [{'id': 'b', 'type': 'Role'}, {'id': 'c', 'type': 'Role'}, {'id': 'd', 'type': 'Role'}]\n",
        "            }\n",
        "        ],\n",
        "        'owners': [],\n",
        "    },\n",
        "]\n",
        "\n",
        "rows = []\n",
        "\n",
        "if TOKEN:\n",
        "    headers = {'Authorization': f'Bearer {TOKEN}'}\n",
        "    apps = requests.get(\n",
        "        'https://graph.microsoft.com/v1.0/applications?$select=id,appId,displayName,requiredResourceAccess',\n",
        "        headers=headers,\n",
        "        timeout=30,\n",
        "    ).json().get('value', [])\n",
        "\n",
        "    for app in apps[:20]:\n",
        "        owners_url = f\"https://graph.microsoft.com/v1.0/applications/{app['id']}/owners?$select=id,displayName,userPrincipalName\"\n",
        "        owners = requests.get(owners_url, headers=headers, timeout=30).json().get('value', [])\n",
        "        owner_names = [o.get('userPrincipalName') or o.get('displayName') for o in owners]\n",
        "        graph_access = [r for r in app.get('requiredResourceAccess', []) if r.get('resourceAppId') == GRAPH_APP_ID]\n",
        "        scopes = sum((r.get('resourceAccess', []) for r in graph_access), [])\n",
        "        rows.append({\n",
        "            'app': app.get('displayName'),\n",
        "            'appId': app.get('appId'),\n",
        "            'graphPermissionCount': len(scopes),\n",
        "            'owners': owner_names or ['NO_OWNER_ASSIGNED'],\n",
        "        })\n",
        "else:\n",
        "    for app in sample_apps:\n",
        "        owner_names = [o.get('userPrincipalName') or o.get('displayName') for o in app.get('owners', [])]\n",
        "        graph_access = [r for r in app.get('requiredResourceAccess', []) if r.get('resourceAppId') == GRAPH_APP_ID]\n",
        "        scopes = sum((r.get('resourceAccess', []) for r in graph_access), [])\n",
        "        rows.append({\n",
        "            'app': app.get('displayName'),\n",
        "            'appId': app.get('appId'),\n",
        "            'graphPermissionCount': len(scopes),\n",
        "            'owners': owner_names or ['NO_OWNER_ASSIGNED'],\n",
        "        })\n",
        "\n",
        "df = pd.DataFrame(rows)\n",
        "print(df.to_string(index=False))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Enumerate delegated consent grants and spot shadow integrations\n",
        "\n",
        "The original post used PowerShell to enumerate delegated OAuth consent grants. This Python version demonstrates the same review logic by analyzing sample grant data and flagging user-consented apps with broad scopes that may have bypassed central review."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import pandas as pd\n",
        "\n",
        "sample_grants = [\n",
        "    {\n",
        "        'AppDisplayName': 'Temporary Assistant App',\n",
        "        'ConsentType': 'Principal',\n",
        "        'PrincipalId': 'user-001',\n",
        "        'ResourceId': 'graph',\n",
        "        'Scope': 'Files.ReadWrite.All Mail.Read User.Read'\n",
        "    },\n",
        "    {\n",
        "        'AppDisplayName': 'HR Workflow Bot',\n",
        "        'ConsentType': 'AllPrincipals',\n",
        "        'PrincipalId': None,\n",
        "        'ResourceId': 'graph',\n",
        "        'Scope': 'User.Read'\n",
        "    },\n",
        "    {\n",
        "        'AppDisplayName': 'OpenAI Notes Helper',\n",
        "        'ConsentType': 'Principal',\n",
        "        'PrincipalId': 'user-002',\n",
        "        'ResourceId': 'graph',\n",
        "        'Scope': 'Sites.ReadWrite.All offline_access'\n",
        "    },\n",
        "]\n",
        "\n",
        "broad_scopes = {\n",
        "    'Directory.ReadWrite.All',\n",
        "    'AppRoleAssignment.ReadWrite.All',\n",
        "    'Mail.ReadWrite',\n",
        "    'Files.ReadWrite.All',\n",
        "    'Sites.ReadWrite.All',\n",
        "}\n",
        "\n",
        "flagged = []\n",
        "for g in sample_grants:\n",
        "    scopes = set(g['Scope'].split())\n",
        "    risky = sorted(scopes & broad_scopes)\n",
        "    if g['ConsentType'] == 'Principal' or risky:\n",
        "        flagged.append({\n",
        "            'AppDisplayName': g['AppDisplayName'],\n",
        "            'ConsentType': g['ConsentType'],\n",
        "            'PrincipalId': g['PrincipalId'],\n",
        "            'RiskyScopes': risky,\n",
        "            'ReviewReason': 'User-consented and/or broad delegated scopes'\n",
        "        })\n",
        "\n",
        "pd.DataFrame(flagged)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Flag risky AI-related apps from an exported inventory CSV\n",
        "\n",
        "This example triages AI-related apps by looking for names associated with agents or copilots, then checking for broad permissions or missing owners. It is useful for quickly reducing governance backlog with a simple exported inventory."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import csv\n",
        "from io import StringIO\n",
        "\n",
        "csv_text = '''AppName,Permissions,Owners\n",
        "Copilot Helper,User.Read;Mail.Read,alice@contoso.com\n",
        "Sales Agent,Files.ReadWrite.All;Mail.Read,\n",
        "OpenAI Research Bot,Sites.ReadWrite.All;User.Read.All,bob@contoso.com\n",
        "Legacy CRM App,User.Read,carol@contoso.com\n",
        "Assistant Sync Tool,Mail.ReadWrite,\n",
        "'''\n",
        "\n",
        "RISKY = {'Mail.ReadWrite', 'Files.ReadWrite.All', 'Sites.ReadWrite.All', 'User.Read.All', 'Directory.ReadWrite.All'}\n",
        "\n",
        "reader = csv.DictReader(StringIO(csv_text))\n",
        "results = []\n",
        "for row in reader:\n",
        "    name = row.get('AppName', '')\n",
        "    scopes = {s.strip() for s in row.get('Permissions', '').split(';') if s.strip()}\n",
        "    owners = [o.strip() for o in row.get('Owners', '').split(';') if o.strip()]\n",
        "    is_ai = any(k in name.lower() for k in ['copilot', 'agent', 'openai', 'assistant', 'bot'])\n",
        "    if is_ai and ((scopes & RISKY) or not owners):\n",
        "        results.append({\n",
        "            'app': name,\n",
        "            'owners': owners or ['NO_OWNER'],\n",
        "            'riskyScopes': sorted(scopes & RISKY),\n",
        "            'reviewReason': 'Broad access or missing owner',\n",
        "        })\n",
        "\n",
        "pd.DataFrame(results)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Review service principals with high-privilege Microsoft Graph access\n",
        "\n",
        "The blog included a PowerShell example for app role assignments. This Python notebook version uses sample service principal assignment data to highlight high-risk Graph app roles such as directory write access, file write access, and site write access."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import pandas as pd\n",
        "\n",
        "assignments = [\n",
        "    {'ServicePrincipal': 'Sales Agent SP', 'AppId': 'sp-001', 'GraphAppRole': 'Files.ReadWrite.All', 'PrincipalType': 'ServicePrincipal'},\n",
        "    {'ServicePrincipal': 'HR Bot SP', 'AppId': 'sp-002', 'GraphAppRole': 'User.Read.All', 'PrincipalType': 'ServicePrincipal'},\n",
        "    {'ServicePrincipal': 'Admin Automation SP', 'AppId': 'sp-003', 'GraphAppRole': 'Directory.ReadWrite.All', 'PrincipalType': 'ServicePrincipal'},\n",
        "    {'ServicePrincipal': 'Reporting App SP', 'AppId': 'sp-004', 'GraphAppRole': 'Mail.Read', 'PrincipalType': 'ServicePrincipal'},\n",
        "]\n",
        "\n",
        "high_risk = {\n",
        "    'Directory.ReadWrite.All',\n",
        "    'AppRoleAssignment.ReadWrite.All',\n",
        "    'Mail.ReadWrite',\n",
        "    'Files.ReadWrite.All',\n",
        "    'Sites.ReadWrite.All',\n",
        "}\n",
        "\n",
        "high_risk_df = pd.DataFrame([a for a in assignments if a['GraphAppRole'] in high_risk])\n",
        "high_risk_df"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Governance review sequence for discovery and control application\n",
        "\n",
        "This sequence summarizes the operational review loop: discover AI apps and agents, map owners and business purpose, inspect permissions, check consent, validate credentials, and then apply controls such as Conditional Access, admin consent workflows, access reviews, and credential rotation."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "review_sequence = [\n",
        "    'Discover AI apps and agents',\n",
        "    'Map owners and business purpose',\n",
        "    'Review delegated and application permissions',\n",
        "    'Check consent model and tenant settings',\n",
        "    'Validate secrets, certs, managed identity use',\n",
        "    'Apply controls: Conditional Access',\n",
        "    'Apply controls: Admin consent workflow',\n",
        "    'Apply controls: Access reviews',\n",
        "    'Apply controls: Credential rotation',\n",
        "]\n",
        "\n",
        "for i, step in enumerate(review_sequence, start=1):\n",
        "    print(f'{i}. {step}')"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Score AI agents against simple Zero Trust controls\n",
        "\n",
        "This example converts the blog's guidance into a lightweight scoring routine. It checks whether an agent has a named owner, whether it uses broad permissions, and whether credentials are close to expiry."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "from datetime import datetime, timezone\n",
        "import pandas as pd\n",
        "\n",
        "agents = [\n",
        "    {'name': 'Copilot Helper', 'owners': ['alice@contoso.com'], 'scopes': ['User.Read'], 'secret_expiry': '2026-01-01T00:00:00Z'},\n",
        "    {'name': 'Sales Agent', 'owners': [], 'scopes': ['Files.ReadWrite.All', 'Mail.Read'], 'secret_expiry': '2025-07-15T00:00:00Z'},\n",
        "    {'name': 'Assistant Sync Tool', 'owners': ['ops@contoso.com'], 'scopes': ['Mail.ReadWrite'], 'secret_expiry': '2025-08-01T00:00:00Z'},\n",
        "]\n",
        "\n",
        "high_risk = {'Files.ReadWrite.All', 'Sites.ReadWrite.All', 'Mail.ReadWrite', 'Directory.ReadWrite.All'}\n",
        "now = datetime.now(timezone.utc)\n",
        "rows = []\n",
        "\n",
        "for a in agents:\n",
        "    expiry = datetime.fromisoformat(a['secret_expiry'].replace('Z', '+00:00'))\n",
        "    findings = []\n",
        "    if not a['owners']:\n",
        "        findings.append('missing_owner')\n",
        "    if high_risk.intersection(a['scopes']):\n",
        "        findings.append('broad_permissions')\n",
        "    if (expiry - now).days < 30:\n",
        "        findings.append('credential_expiring')\n",
        "    rows.append({'agent': a['name'], 'findings': findings or ['baseline_ok']})\n",
        "\n",
        "pd.DataFrame(rows)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Find apps with no owners or aging credentials\n",
        "\n",
        "This notebook version mirrors the PowerShell hygiene check from the post. It identifies applications that have no owners assigned or have secrets or certificates expiring soon, which is especially important for agent-backed apps and service principals."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "from datetime import datetime, timezone\n",
        "import pandas as pd\n",
        "\n",
        "applications = [\n",
        "    {\n",
        "        'DisplayName': 'Copilot Helper',\n",
        "        'AppId': 'app-001',\n",
        "        'Owners': ['alice@contoso.com'],\n",
        "        'PasswordCredentials': ['2026-01-01T00:00:00Z'],\n",
        "        'KeyCredentials': ['2026-06-01T00:00:00Z'],\n",
        "    },\n",
        "    {\n",
        "        'DisplayName': 'Sales Agent',\n",
        "        'AppId': 'app-002',\n",
        "        'Owners': [],\n",
        "        'PasswordCredentials': ['2025-07-20T00:00:00Z'],\n",
        "        'KeyCredentials': [],\n",
        "    },\n",
        "    {\n",
        "        'DisplayName': 'Assistant Sync Tool',\n",
        "        'AppId': 'app-003',\n",
        "        'Owners': ['ops@contoso.com'],\n",
        "        'PasswordCredentials': [],\n",
        "        'KeyCredentials': ['2025-07-25T00:00:00Z'],\n",
        "    },\n",
        "]\n",
        "\n",
        "now = datetime.now(timezone.utc)\n",
        "rows = []\n",
        "for app in applications:\n",
        "    stale_secret = any(datetime.fromisoformat(d.replace('Z', '+00:00')) < now + timedelta(days=30) for d in app['PasswordCredentials'])\n",
        "    stale_cert = any(datetime.fromisoformat(d.replace('Z', '+00:00')) < now + timedelta(days=30) for d in app['KeyCredentials'])\n",
        "    if (len(app['Owners']) == 0) or stale_secret or stale_cert:\n",
        "        rows.append({\n",
        "            'AppName': app['DisplayName'],\n",
        "            'AppId': app['AppId'],\n",
        "            'OwnerCount': len(app['Owners']),\n",
        "            'SecretExpiring': bool(stale_secret),\n",
        "            'CertificateAging': bool(stale_cert),\n",
        "        })\n",
        "\n",
        "pd.DataFrame(rows)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Optional: simple governance maturity self-assessment\n",
        "\n",
        "The post ends with a challenge to rate current agent governance maturity. This quick helper lets you score your environment from 1 to 5 based on whether you have inventory, ownership, permission review, consent governance, telemetry, and human approval checkpoints."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "controls = {\n",
        "    'inventory_exists': True,\n",
        "    'owners_assigned': False,\n",
        "    'permissions_reviewed': False,\n",
        "    'consent_governed': True,\n",
        "    'telemetry_in_place': False,\n",
        "    'human_approval_for_high_impact': True,\n",
        "}\n",
        "\n",
        "score = sum(int(v) for v in controls.values())\n",
        "if score <= 1:\n",
        "    maturity = 1\n",
        "elif score == 2:\n",
        "    maturity = 2\n",
        "elif score in (3, 4):\n",
        "    maturity = 3\n",
        "elif score == 5:\n",
        "    maturity = 4\n",
        "else:\n",
        "    maturity = 5\n",
        "\n",
        "print('Control status:')\n",
        "for k, v in controls.items():\n",
        "    print(f'- {k}: {v}')\n",
        "print(f'\\nEstimated governance maturity: {maturity}/5')"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Next Steps\n",
        "\n",
        "Use this notebook to validate the control order recommended in the post: identity first, consent next, device trust after that, data protection on top, visibility throughout, and autonomy last. In practice, start by inventorying apps and agents, assigning owners, reviewing Graph and Microsoft 365 permissions, tightening consent paths, and then layering in telemetry, DLP, and human approval checkpoints for high-impact workflows."
      ]
    }
  ]
}