API Reference
The Shadow AI Guard API is a REST interface for managing your EDR connector configuration, querying detections, and controlling per-tenant detection policy. The service runs on port 8400 by default. Interactive docs are available at http://<your-host>:8400/docs.
Prerequisites
- Base URL — your organization's Shadow AI Guard deployment (e.g.
https://<your-org>.virtueai.io). ReplaceYOUR_BASE_URLin all examples. - API key — passed in the
X-API-Keyheader, or useAuthorization: Bearer YOUR_TOKENobtained from the Auth endpoints below.
Authentication
POST /auth/login
Exchange credentials for a JWT access token.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
username | string | Yes | Account username |
password | string | Yes | Account password |
tenant_name | string | No | Tenant name (default: "system") |
Response
| Field | Type | Description |
|---|---|---|
access_token | string | JWT bearer token |
refresh_token | string | Token to obtain a new access token |
token_type | string | Always "bearer" |
expires_in | integer | Seconds until access token expires |
- cURL
- Python
- TypeScript
curl -X POST "https://YOUR_BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d '{
"username": "alice@example.com",
"password": "s3cr3t",
"tenant_name": "acme"
}'
import requests
resp = requests.post(
"https://YOUR_BASE_URL/auth/login",
json={"username": "alice@example.com", "password": "s3cr3t", "tenant_name": "acme"},
)
token = resp.json()["access_token"]
const resp = await fetch("https://YOUR_BASE_URL/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "alice@example.com", password: "s3cr3t", tenant_name: "acme" }),
});
const { access_token } = await resp.json();
Expected response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 3600
}
POST /auth/refresh
Exchange a refresh token for a new access token.
Request body: { "refresh_token": "YOUR_TOKEN" }
POST /auth/logout
Invalidate a refresh token.
Request body: { "refresh_token": "YOUR_TOKEN" }
GET /auth/me
Return the authenticated user's profile. Requires Authorization: Bearer YOUR_TOKEN.
Response: { "id": "...", "username": "..." }
POST /auth/register
Register a new user account.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
username | string | Yes | New account username |
password | string | Yes | New account password |
tenant_name | string | No | Tenant to register under (default: "system") |
EDR Connector Configuration
Manage the EDR data source for the tenant (Microsoft Defender, CrowdStrike, SentinelOne).
GET /api/v1/shadow-ai/config
Return the current tenant's active EDR connector configuration. Returns null when no connector has been configured yet.
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_id | string | Override the tenant from the API key (admin use) |
Response
| Field | Type | Description |
|---|---|---|
edr_framework | string | Connector type: "microsoft-defender", "crowdstrike", "crowdstrike-ngsiem", "sentinelone" |
credential_fields | object | Non-secret credential values (IDs, tenant IDs, etc.) |
has_secret | boolean | Whether a secret credential is stored |
settings | object | Connector-specific settings |
created_at | string | null | ISO-8601 creation timestamp |
updated_at | string | null | ISO-8601 last-update timestamp |
- cURL
- Python
- TypeScript
curl "https://YOUR_BASE_URL/api/v1/shadow-ai/config" \
-H "X-API-Key: YOUR_API_KEY"
import requests
resp = requests.get(
"https://YOUR_BASE_URL/api/v1/shadow-ai/config",
headers={"X-API-Key": "YOUR_API_KEY"},
)
print(resp.json())
const resp = await fetch("https://YOUR_BASE_URL/api/v1/shadow-ai/config", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const config = await resp.json();
Expected response:
{
"edr_framework": "microsoft-defender",
"credential_fields": {
"tenant_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"client_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
},
"has_secret": true,
"settings": {},
"created_at": "2025-01-10T09:00:00+00:00",
"updated_at": "2025-03-22T14:30:00+00:00"
}
PUT /api/v1/shadow-ai/config
Create or update the tenant's EDR connector configuration. If a config for the same connector type already exists it is updated in place — secrets already on file are preserved when omitted from the request. Switching to a different connector type disables the previous configuration.
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_id | string | Override the tenant from the API key (admin use) |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
edr_framework | string | Yes | "microsoft-defender", "crowdstrike", "crowdstrike-ngsiem", "sentinelone" |
credentials | object | Yes | Connector credential fields (see table below) |
settings | object | No | Connector-specific settings |
Required credential fields by connector
| Connector | Fields |
|---|---|
microsoft-defender | tenant_id, client_id, client_secret |
crowdstrike | client_id, client_secret, base_url |
crowdstrike-ngsiem | client_id, client_secret, base_url |
sentinelone | base_url, api_token |
Response: same shape as GET /api/v1/shadow-ai/config.
- cURL
- Python
- TypeScript
curl -X PUT "https://YOUR_BASE_URL/api/v1/shadow-ai/config" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"edr_framework": "microsoft-defender",
"credentials": {
"tenant_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"client_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"client_secret": "YOUR_SECRET"
}
}'
import requests
resp = requests.put(
"https://YOUR_BASE_URL/api/v1/shadow-ai/config",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"edr_framework": "microsoft-defender",
"credentials": {
"tenant_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"client_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"client_secret": "YOUR_SECRET",
},
},
)
print(resp.json())
const resp = await fetch("https://YOUR_BASE_URL/api/v1/shadow-ai/config", {
method: "PUT",
headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
edr_framework: "microsoft-defender",
credentials: {
tenant_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
client_id: "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
client_secret: "YOUR_SECRET",
},
}),
});
const config = await resp.json();
Detection Events
GET /api/v1/shadow-ai/events
Return a paginated list of Shadow AI detections for the tenant, ordered by started_at descending. Detections whose detection rule is currently disabled by tenant policy are automatically excluded.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
tenant_id | string | — | Override tenant (admin use) |
start_date | string | — | ISO-8601 — include events at or after this time |
end_date | string | — | ISO-8601 — include events at or before this time |
offset | integer | 0 | Pagination offset |
limit | integer | 50 | Results per page (max 1000) |
detection_mode | string | — | Filter by detection mode |
device_os | string[] | — | Filter by OS (e.g. Windows, macOS). Repeat the parameter for multiple values. |
Response
| Field | Type | Description |
|---|---|---|
events | array | Detection records (see fields below) |
total | integer | Total matching records (for pagination) |
scanned_at | string | ISO-8601 query timestamp |
Event fields
| Field | Type | Description |
|---|---|---|
id | integer | Detection ID |
device_name | string | Hostname where activity was detected |
device_os | string | null | Operating system |
agent_framework | string | Detection rule / framework that matched |
prompt | string | null | Extracted prompt text (when extraction is configured) |
account_name | string | null | User account associated with the activity |
started_at | string | null | ISO-8601 session start |
ended_at | string | null | ISO-8601 session end |
process_count | integer | Process events in the session |
network_count | integer | Network events in the session |
file_count | integer | File events in the session |
total_events | integer | Sum of process + network + file counts |
detection_mode | string | null | Detection mode label |
activity_tags | string[] | Behavioral tags on the session |
action_guard_allowed | boolean | null | Action Guard result (null = not yet evaluated) |
action_guard_violations_count | integer | Number of Action Guard violations |
created_at | string | null | ISO-8601 record creation timestamp |
- cURL
- Python
- TypeScript
curl "https://YOUR_BASE_URL/api/v1/shadow-ai/events?limit=20&start_date=2025-01-01T00:00:00Z" \
-H "X-API-Key: YOUR_API_KEY"
import requests
resp = requests.get(
"https://YOUR_BASE_URL/api/v1/shadow-ai/events",
headers={"X-API-Key": "YOUR_API_KEY"},
params={"limit": 20, "start_date": "2025-01-01T00:00:00Z"},
)
data = resp.json()
print(f"{data['total']} detections found")
for ev in data["events"]:
print(ev["device_name"], ev["agent_framework"], ev["started_at"])
const params = new URLSearchParams({ limit: "20", start_date: "2025-01-01T00:00:00Z" });
const resp = await fetch(
`https://YOUR_BASE_URL/api/v1/shadow-ai/events?${params}`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } },
);
const { events, total } = await resp.json();
GET /api/v1/shadow-ai/events/stats
Return aggregate KPI statistics for the Shadow AI dashboard.
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_id | string | Override tenant (admin use) |
start_date | string | ISO-8601 filter start |
end_date | string | ISO-8601 filter end |
device_os | string[] | Filter by OS |
Response
| Field | Type | Description |
|---|---|---|
total_detections | integer | Total detections in the time window |
top_application | { name: string, count: integer } | Most-detected AI framework and its count |
active_devices_monitored | integer | Number of distinct devices with at least one detection |
- cURL
- Python
- TypeScript
curl "https://YOUR_BASE_URL/api/v1/shadow-ai/events/stats" \
-H "X-API-Key: YOUR_API_KEY"
import requests
resp = requests.get(
"https://YOUR_BASE_URL/api/v1/shadow-ai/events/stats",
headers={"X-API-Key": "YOUR_API_KEY"},
)
print(resp.json())
const resp = await fetch("https://YOUR_BASE_URL/api/v1/shadow-ai/events/stats", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const stats = await resp.json();
Expected response:
{
"total_detections": 142,
"top_application": { "name": "cursor", "count": 58 },
"active_devices_monitored": 23
}
GET /api/v1/shadow-ai/events/{event_id}/tree
Return the full process / network / file event timeline for a single detection.
Path parameters
| Parameter | Type | Description |
|---|---|---|
event_id | integer | Detection ID (from the events list) |
Response
| Field | Type | Description |
|---|---|---|
id | integer | Detection ID |
agent_framework | string | Rule that triggered the detection |
device_name | string | Hostname |
account_name | string | null | User account |
prompt | string | null | Extracted prompt |
started_at | string | null | Session start |
ended_at | string | null | Session end |
process_count | integer | Number of process events |
network_count | integer | Number of network events |
file_count | integer | Number of file events |
events | array | Ordered event timeline (see fields below) |
Timeline event fields
| Field | Type | Description |
|---|---|---|
timestamp | string | ISO-8601 event time |
dimension | string | "Process", "Network", "File", or "DNS" |
detail | string | Human-readable event summary |
device_name | string | Device hostname |
account_name | string | User account |
file_name | string | File name (File events) |
process_command_line | string | Full command line (Process events) |
initiating_process | string | Parent process command line |
- cURL
- Python
- TypeScript
curl "https://YOUR_BASE_URL/api/v1/shadow-ai/events/42/tree" \
-H "X-API-Key: YOUR_API_KEY"
import requests
resp = requests.get(
"https://YOUR_BASE_URL/api/v1/shadow-ai/events/42/tree",
headers={"X-API-Key": "YOUR_API_KEY"},
)
tree = resp.json()
for ev in tree["events"]:
print(ev["timestamp"], ev["dimension"], ev["detail"])
const resp = await fetch("https://YOUR_BASE_URL/api/v1/shadow-ai/events/42/tree", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const tree = await resp.json();
GET /api/v1/shadow-ai/events/{event_id}/guard-result — Coming Soon
Action Guard evaluation is under active development. This endpoint exists in the API today but returns placeholder data until the evaluation pipeline is complete.
Return the Action Guard policy evaluation result for a detection.
Path parameters
| Parameter | Type | Description |
|---|---|---|
event_id | integer | Detection ID |
Response
| Field | Type | Description |
|---|---|---|
evaluated | boolean | Whether Action Guard has run on this detection |
allowed | boolean | null | true = allowed, false = blocked, null = not yet evaluated |
violations | array | Policy violation objects |
violations_count | integer | Number of violations |
explanation | string | null | Human-readable explanation from Action Guard |
POST /api/v1/shadow-ai/evaluate-all — Coming Soon
Bulk re-evaluation of unevaluated detections via Action Guard is not yet implemented. The endpoint accepts requests but always returns { "message": "Not yet implemented" }.
Trigger a re-evaluation pass over all unevaluated detections for the tenant.
Response: { "message": "Not yet implemented", "evaluated": 0, "total": 0 }
AI Connect Awareness
Monitor outbound network connections from managed endpoints to AI provider domains. These endpoints surface connection-level telemetry normalised from EDR network events — giving you visibility into which apps, users, and devices are calling AI APIs regardless of whether a full agent session was detected.
All AI Connect endpoints share a common set of query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
tenant_id | string | — | Override tenant (admin use) |
start | datetime | — | ISO-8601 window start |
end | datetime | — | ISO-8601 window end |
connector | string | — | Filter to a specific EDR connector (e.g. crowdstrike_ngsiem, mde) |
GET /api/v1/ai-connect/summary
Top-line KPI counters for the AI Connect Awareness dashboard.
Response
| Field | Type | Description |
|---|---|---|
total_connections | integer | Total network rows hitting any AI provider domain in the window |
distinct_users | integer | Distinct usernames observed |
distinct_devices | integer | Distinct device UIDs observed |
distinct_providers | integer | Distinct AI providers matched |
window_start | datetime | Start of the query window |
window_end | datetime | End of the query window |
- cURL
- Python
- TypeScript
curl "https://YOUR_BASE_URL/api/v1/ai-connect/summary" \
-H "X-API-Key: YOUR_API_KEY"
import requests
resp = requests.get(
"https://YOUR_BASE_URL/api/v1/ai-connect/summary",
headers={"X-API-Key": "YOUR_API_KEY"},
)
print(resp.json())
const resp = await fetch("https://YOUR_BASE_URL/api/v1/ai-connect/summary", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const summary = await resp.json();
Expected response:
{
"total_connections": 3821,
"distinct_users": 47,
"distinct_devices": 31,
"distinct_providers": 6,
"window_start": "2025-05-12T00:00:00+00:00",
"window_end": "2025-05-19T23:59:59+00:00"
}
GET /api/v1/ai-connect/by-provider
Connection counts grouped and folded by AI provider. Domain-level rows are aggregated in SQL then mapped to named providers in application code.
Additional query parameters
(none beyond the common set above)
Response
| Field | Type | Description |
|---|---|---|
providers | array | Per-provider breakdown items (see below) |
window_start | datetime | Start of the query window |
window_end | datetime | End of the query window |
Provider breakdown item fields
| Field | Type | Description |
|---|---|---|
provider_id | string | Rule ID that matched (e.g. ai-connect-anthropic) |
provider_name | string | Human-readable provider name |
connection_count | integer | Total connections to this provider |
distinct_users | integer | Distinct users that connected |
distinct_devices | integer | Distinct devices that connected |
last_seen | datetime | null | Most recent connection time |
- cURL
- Python
- TypeScript
curl "https://YOUR_BASE_URL/api/v1/ai-connect/by-provider" \
-H "X-API-Key: YOUR_API_KEY"
import requests
resp = requests.get(
"https://YOUR_BASE_URL/api/v1/ai-connect/by-provider",
headers={"X-API-Key": "YOUR_API_KEY"},
)
for provider in resp.json()["providers"]:
print(provider["provider_name"], provider["connection_count"])
const resp = await fetch("https://YOUR_BASE_URL/api/v1/ai-connect/by-provider", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { providers } = await resp.json();
GET /api/v1/ai-connect/by-app
Connection counts grouped by the initiating application process name. actor_process_name is promoted to a dedicated column at ingest time, making this aggregation a cheap GROUP BY even over large windows.
Additional query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Max apps to return (max 200) |
Response
| Field | Type | Description |
|---|---|---|
apps | array | Per-application breakdown items (see below) |
window_start | datetime | Start of the query window |
window_end | datetime | End of the query window |
App breakdown item fields
| Field | Type | Description |
|---|---|---|
app_name | string | Process name (e.g. msedge.exe) |
connection_count | integer | Total connections from this app |
distinct_providers | integer | Distinct AI providers contacted |
distinct_users | integer | Distinct users running this app |
last_seen | datetime | null | Most recent connection time |
- cURL
- Python
- TypeScript
curl "https://YOUR_BASE_URL/api/v1/ai-connect/by-app?limit=10" \
-H "X-API-Key: YOUR_API_KEY"
import requests
resp = requests.get(
"https://YOUR_BASE_URL/api/v1/ai-connect/by-app",
headers={"X-API-Key": "YOUR_API_KEY"},
params={"limit": 10},
)
for app in resp.json()["apps"]:
print(app["app_name"], app["connection_count"])
const resp = await fetch("https://YOUR_BASE_URL/api/v1/ai-connect/by-app?limit=10", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { apps } = await resp.json();
GET /api/v1/ai-connect/events
Paginated row-level connection feed for the dashboard table.
Additional query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Results per page (max 500) |
provider_id | string | — | Filter to a single AI provider (e.g. ai-connect-anthropic) |
Response
| Field | Type | Description |
|---|---|---|
events | array | Connection event records (see below) |
total | integer | Total matching records |
window_start | datetime | Start of the query window |
window_end | datetime | End of the query window |
Connection event fields
| Field | Type | Description |
|---|---|---|
time | datetime | Event timestamp |
connector_id | string | EDR connector that sourced this row (e.g. crowdstrike_ngsiem, mde) |
device_uid | string | Device identifier |
device_name | string | null | Device hostname |
user_name | string | null | Username |
app_name | string | null | Initiating application process name |
app_cmd_line | string | null | Full command line of the initiating process |
dst_domain | string | null | Destination domain |
dst_ip | string | null | Destination IP address |
dst_port | integer | null | Destination port |
provider_id | string | null | AI provider rule ID matched |
provider_name | string | null | AI provider human-readable name |
- cURL
- Python
- TypeScript
curl "https://YOUR_BASE_URL/api/v1/ai-connect/events?limit=20&provider_id=ai-connect-openai" \
-H "X-API-Key: YOUR_API_KEY"
import requests
resp = requests.get(
"https://YOUR_BASE_URL/api/v1/ai-connect/events",
headers={"X-API-Key": "YOUR_API_KEY"},
params={"limit": 20, "provider_id": "ai-connect-openai"},
)
data = resp.json()
print(f"{data['total']} events found")
for ev in data["events"]:
print(ev["time"], ev["user_name"], ev["app_name"], "