Shadow AI Discovery
The gateway governs traffic that points at it. Shadow AI Discovery answers the other question: what AI is running in the organization that doesn’t? Collectors and scanner adapters run in your infrastructure — CI, a host, a scheduled job — and send cryptographically signed discovery events to the platform, which reconciles them against the real gateway configuration and surfaces the result in the Asset Register as governed, ungoverned or unknown.

How events are ingested
Section titled “How events are ingested”Collectors POST events to POST /v1/discovery/events on the Control Plane. There is no admin JWT on this endpoint — the Ed25519 signature is the authentication:
- Register a collector key (
POST /v1/admin/discovery/collector-keys) — a base64 Ed25519 public key with atrust_tier:first_party·channel·customer·community. - The collector signs each event (canonical JSON, minus the signature block) with its private key and sends
signature: {alg: "ed25519", key_id, value}. - The platform verifies the signature against the registered key. Missing/bad signature or unknown/revoked key →
401.
Idempotency and dedup are built in: event_id is unique per tenant (a duplicate is a {"status": "duplicate"} no-op), and events deduplicate into one discovered asset per (tenant, asset_key) — re-discovering a dismissed asset re-activates it.
Two properties are platform-computed and never taken from the event:
- Confidence — strongest evidence wins (passive network 0.9 > active probe 0.7 > host-derived 0.5), multiplied by the collector key’s trust tier. A collector’s own confidence claim is ignored.
- Governance status — reconciled against live config: an
llm_endpointis matched tollm_providersby host, anmcp_servertomcp_servers.base_url, ana2a_agenttoagent_cards.source_url. Matched to an enabled row →governed; unmatched or disabled →ungoverned; no host to match →unknown.
The egress-log collector
Section titled “The egress-log collector”The broadest first-party collector reads destination hostnames from egress proxy, firewall or DNS logs (Zscaler, Palo Alto, Squid) and emits an llm_endpoint event per known AI-provider host — OpenAI, Azure OpenAI, Anthropic, Google (Gemini + Vertex), AWS Bedrock, Mistral, Cohere, Groq, Perplexity.
pip install brutor-discoverybrutor-egress-collector --input /var/log/squid/access.log \ --gateway https://control.example.com --key collector.keyEvidence lands as tls_sni, passive — the highest-confidence class.
The endpoint collector (Brutor Scout)
Section titled “The endpoint collector (Brutor Scout)”Where the egress collector sees the network, Brutor Scout sees the workstation. It’s a small signed-event collector you run on a developer’s machine (one-shot or on a timer). It inventories the AI clients installed there — Claude Code, Claude Desktop, Cursor, VS Code, Codex, Goose, Windsurf — and, crucially, parses each client’s MCP server configuration, so the unmanaged MCP servers developers have wired up surface as discovered assets too. Purely read-only filesystem inspection; it never intercepts traffic or reads payloads.
Scout emits two asset types, both observe-only:
local_ai_client— an AI client found on a devicelocal_mcp_server— an MCP server a client is configured to use (the shadow-MCP surface), with an endpoint block for remote (http) servers
-
Mint a collector key (once per device):
Terminal window brutor-scout keygen --collector-id scout-$(hostname)# → register the printed Public key via POST /v1/admin/discovery/collector-keys# → store the Seed as BRUTOR_SCOUT_SEED -
Preview without emitting:
Terminal window brutor-scout scan --dry-run --tenant acme -
Emit to the gateway (idempotent — re-running only advances
last_seen):Terminal window export BRUTOR_SCOUT_SEED=…brutor-scout scan --gateway https://control.example.com \--tenant acme --collector-id scout-$(hostname)
Discovered devices roll up in the Admin UI under Asset Register → Devices: one row per device with its AI-client and MCP-server counts, drilling into the clients on a device and the MCP servers each is wired to. Evidence lands as config_file, host-derived.
Local tool-call audit
Section titled “Local tool-call audit”Inventory tells you a coding agent is installed. It does not tell you whether anyone uses it, or which of those unmanaged MCP servers they actually call. Scout can answer that too, by registering hooks the agent invokes before and after every tool call:
brutor-scout hooks install # merges into ~/.claude/settings.jsonbrutor-scout hooks status # where the spool is, how much is queuedbrutor-scout submit --gateway https://gateway.acme.com --tenant acmeRecords spool locally and are batch-submitted under the same Ed25519 signature
as every other discovery event. Delivery is at-least-once — a laptop that
sleeps mid-upload re-sends, and the gateway deduplicates on event_id, so a
reconnecting device drains its backlog without double-counting.
Two more design points worth knowing:
- The hook always exits 0, including on failure. It runs in the developer’s critical path on every tool call; a hook that can block a tool is a hook that gets uninstalled. A dropped audit record is the cheaper failure.
- Installing merges rather than overwrites. Hooks a developer already has are preserved, and re-installing replaces our entry instead of stacking duplicates.
The payoff is a question a gateway structurally cannot answer:
| MCP server | Local calls this week |
|---|---|
github |
125 |
filesystem |
75 |
nordica-claims-core |
67 |
notion |
48 |
An inventory row nobody calls is noise. One called hundreds of times a week is
a priority — and GET /v1/admin/discovery/agent-events returns exactly that
roll-up alongside the raw records.
BYO scanners: the ingest adapters
Section titled “BYO scanners: the ingest adapters”Already running an AI scanner? brutor-ingest converts its output into signed discovery events — no bespoke collector required. Four adapters ship in the Discovery SDK (catalog: GET /v1/admin/discovery/ingest-adapters):
| Adapter | Modality | Ingests |
|---|---|---|
cyclonedx-aibom |
static | CycloneDX 1.5/1.6 ML/AI-BOM from any code/container scanner (Cisco AI-BOM, Trusera, cdxgen) — models, SDKs, agent frameworks, MCP servers |
agentsonar |
runtime | Knostic AgentSonar process→domain AI classifications — traffic bypassing the gateway |
snyk-agent-scan |
config | Snyk agent-scan / mcp-scan — MCP servers from local client configs (Claude Desktop, Cursor, …) incl. tool-poisoning findings, skills, agents |
safedep-vet |
static | SafeDep vet ai discover / vet code scan — coding agents, MCP servers, IDE extensions, AI-SDK usage in Go/Python/JS-TS |
# Feed a CycloneDX AI-BOM from CIbrutor-ingest --adapter cyclonedx-aibom -i aibom.json
# Feed AgentSonar runtime classifications, thresholdedbrutor-ingest --adapter agentsonar -i pairs.jsonl --set min_ai_score=0.6What a discovered asset looks like
Section titled “What a discovered asset looks like”asset_type ∈ llm_endpoint · mcp_server · a2a_agent · inference_runtime · agent_framework · sdk_usage · unknown_ai, with provider/endpoint details, computed_confidence, governance_status (+ governed_ref when matched), lifecycle status (active · onboarded · dismissed), evidence count and first/last seen. Evidence rows carry the type (dns_resolution, tls_sni, netflow, process, env_var, package_manifest, config_file, well_known_probe, registry_entry, api_inventory), the collector and its trust tier.
Acting on discoveries
Section titled “Acting on discoveries”| Method | Path | Purpose |
|---|---|---|
| GET | /v1/admin/discovery/assets |
List discovered assets + coverage counts |
| GET | /v1/admin/discovery/coverage |
The coverage attestation (see below) |
| GET | /v1/admin/discovery/agent-events |
Local tool-call activity + most-called MCP servers |
| POST | /v1/discovery/agent-events |
Scout submits a signed batch (signature-auth, no admin JWT) |
| GET | /v1/admin/discovery/assets/{id} |
Detail with evidence |
| POST | /v1/admin/discovery/assets/{id}/onboard |
Returns a pre-fill config payload; marks onboarded |
| POST | /v1/admin/discovery/assets/{id}/dismiss |
Mark not-relevant |
| POST | /v1/admin/discovery/reconcile |
Re-run governance matching for all assets |
| GET / POST | /v1/admin/discovery/collector-keys |
Manage collector keys (POST .../{key_id}/revoke to revoke) |
curl -s -X POST http://localhost:5050/v1/admin/discovery/assets/dasset-01K0.../onboard \ -H "Authorization: Bearer $ADMIN_JWT"{ "status": "onboarded", "prefill": { "kind": "mcp_server", "name": "internal-crm-mcp", "base_url": "http://crm-tools.internal:8080" }}Coverage attestation
Section titled “Coverage attestation”Discovery’s real output is not a list of assets — it is the answer to what fraction of our AI activity is actually governed? That answer is the population proof under every Assurance Report: a report evidences the traffic it observed, and coverage states how much of the estate that was.
curl -s http://localhost:5050/v1/admin/discovery/coverage \ -H "Authorization: Bearer $ADMIN_JWT"{ "basis": "measured", "coverage_pct": 0.87, "governed": 27, "ungoverned": 3, "unknown": 1, "dismissed": 4, "endpoint_plane_observed": false, "statement": "87% of the 31 discovered AI assets are routed through the gateway and therefore in scope for this report. 4 are not, and nothing in this report evidences their behaviour."}unknown (unreconciled) assets count against coverage — an asset we could not match
to a configured route is not a routed asset. Dismissed assets leave the denominator but
are reported alongside it, because an operator can raise the percentage by dismissing and
that must stay auditable. A collector that has gone quiet for more than seven days
degrades basis to stale: the ratio is still real, but it describes the estate as last
seen.
The attestation lives on Discovery Integrations → Coverage in the Admin Console, and
is embedded in the Assurance Report’s coverage section and its
not_covered gaps.
Discoveries surface in two places: the Asset Register (the ungoverned list, coverage scorecard, and onboard/dismiss actions — Discovery Integrations is a drill-down from its header) and Mission Control’s Governance tab (shadow-AI signals).
Related
Section titled “Related”- Asset Register — where discoveries land
- Traffic Data Import — the complementary observed plane (vendor-reported usage)
- Register your own MCP server — the onboarding target for a discovered MCP server

