VII.4VII · The programdefense
Finding the AI you do not know about
You cannot threat-model, secure, or retire a system you have never heard of, so discovery is the control that precedes every other control in this part - and it takes five independent feeds, because none of them sees all of it.
VII.3 · Detection, IR & forensics for AI makes the case for why discovery matters. This page is the runbook.
flowchart TB F1["1 · Egress and SaaS<br/>proxy, SWG, CASB, DNS"] --> INV["AI inventory / AIBOM<br/>owner · data classes · identity<br/>· store · retire-by"] F2["2 · Cloud control plane<br/>CloudTrail · Azure diagnostics<br/>· Vertex audit logs · GPU spend"] --> INV F3["3 · Developer surface<br/>extensions · IDE plugins<br/>· coding agents · MCP configs"] --> INV F4["4 · Identity<br/>OAuth grants · service principals<br/>· GitHub Apps · IAM roles"] --> INV F5["5 · Money and people<br/>expense · procurement<br/>· support tickets"] --> INV INV --> ACT["Sanction · restrict<br/>· migrate · block"] classDef d fill:#0f1a18,stroke:#5bd1c5,color:#bdeee2; classDef l fill:#1a160f,stroke:#e4a23f,color:#f0d9ad; class F1,F2,F3,F4,F5,ACT d; class INV l;
The inventory is amber because it is the only node that is not optional. Skip any feed and you still find things; skip the inventory and every feed produces a report that ages out in a week.
Feed 1 - egress and SaaS telemetry
What matters is not “someone visited chatgpt.com.” It is programmatic model-API traffic, because that means an application, not a person, and applications persist.
The signature is distinctive enough to hunt without decrypting anything: repeated POST to a small set of provider hostnames, an Authorization: Bearer header, a small request and a long-lived streamed response held open for tens of seconds, from a server or build agent rather than a laptop.
-- Build the hostname list from each provider's own API reference for your region.-- Only api.anthropic.com is confirmed from a primary vendor page in this pass; add the-- others yourself and include the aggregators and gateways (they are how model traffic-- escapes a naive allowlist), then keep the list under change control.`proxy_logs`| eval is_model_api = if(match(dest_host, "(?i)^(api\.anthropic\.com|<your-verified-hosts>)$"), 1, 0)| where is_model_api = 1| stats count as calls, sum(bytes_out) as bytes_sent, dc(dest_host) as providers, earliest(_time) as first_seen, latest(_time) as last_seen by src_ip, user, http_user_agent| eval is_server = if(match(http_user_agent, "(?i)python|node|curl|okhttp|go-http|axios"), 1, 0)| where is_server = 1 OR calls > 200 /* automation, or a human at machine volume */| sort - bytes_sent /* rank by data leaving, not by call count */Rank by bytes sent, not request count. A hundred thousand short calls is a chatbot; forty megabytes outbound in an afternoon is a document corpus leaving the building.
If you run Microsoft Entra Global Secure Access, the vendor feed is built. Its shadow AI discovery analyzes internet and Microsoft 365 traffic and identifies generative AI applications, AI Model Provider APIs, and SaaS MCP servers, matching them against the Defender for Cloud Apps catalog for risk scoring; it surfaces under Global Secure Access, Applications, Insights and Analytics, filtered by “Generative AI apps and tools,” and requires the Global Secure Access Log Reader role. Microsoft distinguishes it from the separate Generative AI Insights capability, which “uses TLS inspection and deep packet inspection to log the actual prompt content and Model Context Protocol (MCP) operations.” Put that distinction in front of a client: application-level inventory is a discovery control; prompt-level logging is a data-protection control with its own privacy consequences, and they should be approved separately.
Feed 2 - your own cloud control plane
Managed model APIs never cross a proxy.
# AWS - InvokeModel, InvokeModelWithResponseStream, Converse, ConverseStream and# ListAsyncInvokes are MANAGEMENT events, already in CloudTrail with no extra config.aws cloudtrail lookup-events --lookup-attributes \ AttributeKey=EventSource,AttributeValue=bedrock.amazonaws.com \ --start-time "$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)" \ --query 'Events[].{t:EventTime,name:EventName,who:Username}' --output table# AGENTIC invocation is a DATA event and is NOT captured by default. Add advanced event# selectors for the resource type that matches the call you care about, or you stay blind# to exactly the workloads with the largest blast radius:# InvokeAgent -> AWS::Bedrock::AgentAlias# InvokeInlineAgent -> AWS::Bedrock::InlineAgent# Retrieve / RetrieveAndGenerate -> AWS::Bedrock::KnowledgeBase# InvokeFlow -> AWS::Bedrock::FlowAlias# (AWS::Bedrock::Model and AWS::Bedrock::AsyncInvoke cover bidirectional streaming and# async invoke - useful, but they do not cover agents.)
# Azure - Foundry Tools diagnostic logging must be enabled before anything reaches a# workspace. Portal categories are Audit, RequestResponse and AllMetrics. Absence of# results here means absence of logging, not absence of usage.az monitor diagnostic-settings list --resource <ai-services-resource-id> -o table# KQL once enabled:# AzureDiagnostics | where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"# | summarize calls=count() by Resource, OperationName, bin(TimeGenerated, 1d)
# GCP - Vertex AI admin activity is on by default; data access needs enabling.gcloud logging read 'protoPayload.serviceName="aiplatform.googleapis.com"' \ --freshness=30d \ --format='value(protoPayload.methodName,protoPayload.authenticationInfo.principalEmail)' \ | sort | uniq -c | sort -rn
# The blunt instrument that catches what the above misses: unexplained GPU spend.aws ce get-cost-and-usage --time-period Start=<start>,End=<end> --granularity MONTHLY \ --metrics UnblendedCost --group-by Type=DIMENSION,Key=USAGE_TYPE \ | grep -Ei 'p4d|p5|g5|g6|inf|trn|Bedrock'The AWS asymmetry is worth internalizing: plain model invocation shows up for free, agentic invocation does not (VII.2).
Feed 3 - the developer surface
Browser extensions, IDE plugins and coding agents are shadow AI with credentials and a filesystem (III.2 · Coding agents & Codex security). They leave the most legible artifacts on disk.
# 1) VS Code extensions per machine (--show-versions gives a diffable baseline)code --list-extensions --show-versions
# 2) MCP servers configured on machines and committed into repos.# Claude Code: project scope in .mcp.json at the repo root; local and user scope in# ~/.claude.json. VS Code: .vscode/mcp.json plus a user-profile mcp.json# ("MCP: Open User Configuration"). Other clients use their own per-tool paths.fd -H -t f '^(\.mcp\.json|mcp\.json)$' <repos>/ ~/rg -l '"mcpServers"' <repos>/ ~/.claude.json 2>/dev/nullclaude mcp list # what THIS machine will actually connect to
# 3) Agent instruction files are a reliable tell that an agent is in the loop at allfd -H -t f '^(CLAUDE\.md|AGENTS\.md|SKILL\.md)$' <repos>/
# 4) AI SDKs entering an existing application, plus the keys that come with themrg -i --pcre2 'import\s+(openai|anthropic|langchain|litellm|google\.generativeai)' <repos>/trufflehog filesystem <repos>/ --only-verifiedAn .mcp.json committed to a repository is a shared, version-controlled grant of tool access to whoever runs that repository - Claude Code’s own docs describe project scope as “designed to be checked into version control.” Treat finding one as finding an undocumented integration, and read what it connects to first (IV.2 · Model Context Protocol (MCP)).
Feed 4 - the agent nobody registered
Network discovery misses OAuth-connected agents entirely: consent is granted once and the access lives in the SaaS provider’s back end forever.
# Entra: third-party enterprise apps and the permissions they hold. App-role# (application) permissions are the dangerous ones - they act without a user.az ad sp list --all --query "[?tags[?contains(@,'WindowsAzureActiveDirectoryIntegratedApp')]]\.{name:displayName,appId:appId,created:createdDateTime}" -o table# Graph, for what each was actually granted:# GET /oauth2PermissionGrants (delegated - acts as a user)# GET /servicePrincipals/{id}/appRoleAssignments (application - acts on its own)
# GitHub: every App installed on the org. Requires org owner and the admin:read scope.gh api /orgs/<org>/installations --jq '.installations[]\| {app:.app_slug, repos:.repository_selection, created:.created_at}'
# AWS: roles that exist but have not been used - orphaned NHIs from dead pilotsaws iam list-roles --query 'Roles[].RoleName' --output text | tr '\t' '\n' | \ while read r; do aws iam get-role --role-name "$r" \ --query 'Role.{name:RoleName,last:RoleLastUsed.LastUsedDate}' --output text done | awk '$2 == "None" || $2 < "'"$(date -u -d '90 days ago' +%Y-%m-%d)"'"'Three questions per result. Treat a blank as a finding, not a gap in your data.
- Who owns it? A named person, not a team alias and not “the AI team.”
- What can it reach? Scopes, app roles, repository selection, IAM policy (IV.6 · Agent identity & access (NHI)).
- When does it die? No answer means you have found the input to VII.5 · Retirement & decommissioning.
Feed 5 - money and people
The least technical feed has the best precision. Card and expense records show AI subscriptions charged to individuals. Procurement shows tools bought as a “productivity pilot” that never reached architecture review. Support tickets name tools users assume are approved. Run discovery inside the existing IT-audit cycle rather than as a one-off project - that is the difference between a program and an exercise.
From a list to an inventory
| Field | Why it is on the list |
|---|---|
| System, owner (a person), business purpose | Accountability; without it nothing else gets fixed |
| Model and version, provider, hosting posture | Vendor assurance and provider-incident blast radius |
| Data classes it reads and writes | Whether this is a privacy matter or a productivity one |
| Identity used, and its scopes | The containment lever during an incident (VII.3) |
| Stores it created (vector DB, prompt logs, checkpoints) | The data exhaust that outlives it (V.3) |
| Retention and retire-by date | Turns discovery into a lifecycle instead of a snapshot |