Shadow AI discovery
Find the unsanctioned AI tools your workforce is already using. Ship egress, SSO, or CASB logs to EvalGuard, and it classifies every domain against a known-AI-tool catalog, rolls sightings up per tool, and ranks them by usage and data risk.
"Shadow AI" is AI-SaaS used outside IT approval — ChatGPT, Claude, Otter, Fireflies, and hundreds more. Discovery answers the question a policy alone can't: who is actually pasting company data into which tool.
What ships today
The full discovery pipeline is live: log ingest from Zscaler / Netskope / Cloudflare Gateway / Okta, domain classification, the detections rollup, per-domain policy overrides, a browsable AI-app catalog, and the evalguard shadow-ai CLI. The single-request PII/sensitive-data analyzer is live too. The aggregate GET /api/v1/shadow-ai dashboard is still a thin roll-up over gateway logs — see Honest status below.
How discovery works
You already have the raw signal — your secure web gateway, CASB, or IdP logs every outbound request. EvalGuard turns that noise into a ranked inventory in three steps:
1. INGEST POST egress-log rows → classify each domain against the
AI-tool catalog (chat.openai.com → ChatGPT, restricted, high risk)
→ upsert into shadow_ai_sightings, summing request_count + bytes.
2. DETECT GET rolls deduped sightings up per tool/domain, ranked by
request volume + data risk, with affected users / teams /
departments and an "unsanctioned" flag.
3. GOVERN Set a per-domain policy (approved | blocked | pending). It
propagates to sightings instantly and silences (or raises)
violation alerts. New sightings fire shadow_ai_detected playbooks.Classification is hostname-first (what external logs actually give you) and walks up the domain tree, so chat.openai.com resolves to the same tool as openai.com. Domains that aren't in the catalog are counted as domain_not_in_catalog skips rather than silently dropped.
1 · Ingest egress logs
POST /api/v1/shadow-ai/ingest accepts up to 10,000 rows per call from five source formats: zscaler, netskope, cloudflare, okta, or generic. The parser is tolerant — it maps each vendor's field names to a normalized sighting and skips (never throws on) malformed rows. Callers typically run this on a cron pulling the last N minutes of logs.
curl -X POST https://evalguard.ai/api/v1/shadow-ai/ingest \
-H "Authorization: Bearer $EVALGUARD_API_KEY" \
-H "content-type: application/json" \
-d '{
"source": "cloudflare",
"projectId": "proj_123",
"rows": [
{ "UserEmail": "dana@acme.com", "HTTPHost": "chat.openai.com",
"Timestamp": "2026-07-15T09:12:00Z", "BytesReceived": 20481 },
{ "UserEmail": "lee@acme.com", "HTTPHost": "otter.ai",
"Timestamp": "2026-07-15T09:20:00Z", "BytesReceived": 88213 }
]
}'
# → 201
# {
# "success": true,
# "data": {
# "ingested": 2, "newSightings": 2, "updatedSightings": 0,
# "parsedRows": 2, "skipped": 0, "byReason": {}
# }
# }Re-ingesting the same (project, domain, user, source) tuple sums counts rather than overwriting them (via the shadow_ai_upsert_sightings RPC), so daily log-shipping produces correct cumulative totals. Rate-limited to 10 req/min; the supplied projectId is verified to belong to the authenticated org before any write.
2 · Read the detections rollup
GET /api/v1/shadow-ai/detections aggregates the deduped sightings per tool and ranks them by request volume. Filter with category (approved / restricted / blocked / unclassified), risk (minimum data risk), status, and limit.
curl -H "Authorization: Bearer $EVALGUARD_API_KEY" \
"https://evalguard.ai/api/v1/shadow-ai/detections?projectId=proj_123&risk=high"
# → { "success": true, "data": {
# "detections": [
# {
# "toolName": "Otter.ai", "domain": "otter.ai",
# "category": "restricted", "dataRisk": "critical",
# "policyStatus": "unreviewed", "unsanctioned": true,
# "userCount": 14, "requestCount": 2210, "bytesTransferred": 41288810,
# "departments": ["Sales"], "teams": ["AE-West"],
# "firstSeen": "2026-07-01T...", "lastSeen": "2026-07-15T..."
# }
# ],
# "summary": {
# "totalTools": 23, "unsanctionedTools": 9, "totalUsers": 118,
# "totalRequests": 40213, "highRiskTools": 6,
# "scannedSightings": 8801, "capped": false
# }
# } }A tool is flagged unsanctioned when its catalog category is restricted or blocked AND no explicit approved policy exists for it. The rollup scans up to 10,000 sightings per request; summary.capped tells you when a very large tenant has hit that ceiling (a SQL-side rollup is a future optimization).
3 · Govern with per-domain policy
A policy override pins a (project, domain) pair to approved, blocked, or pending, taking precedence over the catalog default. Setting approved on grammarly.com stops it surfacing as a violation; the change propagates to existing sightings immediately, so the detections view flips without a re-ingest.
# Approve a tool (admin-only, audited as shadow_ai_policy_set)
curl -X POST https://evalguard.ai/api/v1/shadow-ai/policy \
-H "Authorization: Bearer $EVALGUARD_API_KEY" \
-H "content-type: application/json" \
-d '{ "projectId": "proj_123", "domain": "grammarly.com",
"status": "approved", "rationale": "Enterprise DLP-covered plan" }'
# List every override for a project
curl -H "Authorization: Bearer $EVALGUARD_API_KEY" \
"https://evalguard.ai/api/v1/shadow-ai/policy?projectId=proj_123"
# Remove an override (reverts affected sightings to 'unreviewed')
curl -X DELETE -H "Authorization: Bearer $EVALGUARD_API_KEY" \
"https://evalguard.ai/api/v1/shadow-ai/policy?projectId=proj_123&domain=grammarly.com"POST and DELETE require the admin role and are written to the audit log — deciding what AI a company may use is a governance action, not a casual toggle.
Do it all from the CLI
The evalguard shadow-ai command wraps the same endpoints — point it at an NDJSON export (one JSON object per line) and it chunks the upload for you.
# Upload an egress-log export, classified on ingest evalguard shadow-ai upload egress.ndjson --source cloudflare # Rank detected tools by request volume (add --json to pipe it) evalguard shadow-ai detections --project proj_123 --risk high # Otter.ai critical users=14 reqs=2210 ⚠ unsanctioned # ChatGPT high users=41 reqs=8830 ⚠ unsanctioned # Set / list per-domain policy evalguard shadow-ai policy set chat.openai.com --status blocked \ --project proj_123 --rationale "No enterprise agreement" evalguard shadow-ai policy list --project proj_123
Browse the AI-app catalog
GET /api/v1/shadow-ai/catalog exposes the reference set of known AI applications behind discovery — searchable by name, provider, or domain, and filterable by category (chatbot, coding, image, audio, and more). Each entry carries a data-risk rating and a trainOnData flag so you can reason about exposure per tool.
curl -H "Authorization: Bearer $EVALGUARD_API_KEY" \
"https://evalguard.ai/api/v1/shadow-ai/catalog?category=chatbot&search=deep"
# → { "success": true, "data": {
# "apps": [ { "id": "deepseek", "name": "DeepSeek", "provider": "DeepSeek",
# "domain": "chat.deepseek.com", "category": "chatbot",
# "dataRisk": "critical", "trainOnData": true } ],
# "totalResults": 1, "totalCatalog": 200 } }Inline request analysis
Separately from log-based discovery, POST /api/v1/shadow-aiscores a single prompt for leak risk — useful when EvalGuard's gateway is inline and you want a verdict on one request. It runs PII and sensitive-data detection and returns a 0–100 risk score plus an action.
curl -X POST https://evalguard.ai/api/v1/shadow-ai \
-H "Authorization: Bearer $EVALGUARD_API_KEY" \
-H "content-type: application/json" \
-d '{ "input": "Summarize this record: Jane Doe, SSN 123-45-6789",
"provider": "openai", "model": "gpt-4o" }'
# → { "success": true, "data": {
# "event": { "authorized": true, "piiDetected": true, "piiTypes": ["ssn"],
# "sensitiveDataDetected": false, "riskScore": 35,
# "action": "allowed" },
# "piiDetails": { "detected": true, "types": ["ssn"] },
# "sensitiveDataDetails": { "detected": false, "types": [] }
# } }Risk weighting: unauthorized model +30, PII +25, sensitive data +25, extra bumps for private keys / AWS keys / large payloads. Score ≥ 50 is flagged; an unauthorized model under a blockUnknown policy is blocked.
Acting on findings
Discovery is only useful if it drives action. The intended loop:
- Triage by risk, not volume alone. A single Otter.ai user (critical, records meetings) can outrank a busy but low-risk translation tool.
- Decide per domain. Approve tools you can govern (with a rationale for the audit trail), block the ones you can't, and leave the rest
pendingwhile you evaluate. - Automate the alert. Newly seen tools/users fire the
shadow_ai_detectedplaybook with a severity derived from how many new sightings landed — wire it to Slack, a ticket, or a webhook. Re-ingests of known tuples don't re-alert, so you avoid a flood. - Close the loop. Once a policy is set, the detections dashboard reflects it immediately, so approved tools drop out of the unsanctioned count on the next view.
Honest status
What's partial
- The aggregate
GET /api/v1/shadow-aidashboard currently returns model/user counts over recentgateway_logsonly. ItsunauthorizedUsage,piiLeakAttempts, andrequestsBlockedfields are placeholders (all traffic lands in thelowrisk bucket). The real, risk-weighted signal lives in the ingest → detections pipeline above — use that. - Ingest classification runs against a focused catalog of the highest-risk AI tools. The larger browsable set behind
/catalogis a superset; a domain only becomes a sighting if the classifier recognizes it, so unknown AI domains show up asdomain_not_in_catalogskips until added. - The detections rollup is computed in-handler with a 10k-sighting scan cap per request. A SQL-side materialized rollup for very large tenants is on the roadmap;
summary.cappedsignals when you've hit the ceiling today.