Skip to content

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.

Shadow model-API egress hunt over proxy / SWG logs (Splunk-style; adapt field names)
-- 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.

Find managed-model usage nobody registered, in accounts you already own
# 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.

Inventory AI tooling on managed endpoints and in repos
# 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/null
claude 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 all
fd -H -t f '^(CLAUDE\.md|AGENTS\.md|SKILL\.md)$' <repos>/
# 4) AI SDKs entering an existing application, plus the keys that come with them
rg -i --pcre2 'import\s+(openai|anthropic|langchain|litellm|google\.generativeai)' <repos>/
trufflehog filesystem <repos>/ --only-verified

An .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.

Enumerate the non-human identities that can act on your data
# 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 pilots
aws 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.

  1. Who owns it? A named person, not a team alias and not “the AI team.”
  2. What can it reach? Scopes, app roles, repository selection, IAM policy (IV.6 · Agent identity & access (NHI)).
  3. 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

FieldWhy it is on the list
System, owner (a person), business purposeAccountability; without it nothing else gets fixed
Model and version, provider, hosting postureVendor assurance and provider-incident blast radius
Data classes it reads and writesWhether this is a privacy matter or a productivity one
Identity used, and its scopesThe 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 dateTurns discovery into a lifecycle instead of a snapshot