Netskope AI SecOps can deliver Cases, User Risk, and AI Risk events to Microsoft Sentinel in near real time through Outbound Webhooks and the Azure Monitor Logs Ingestion API. This article walks through the full setup: the Azure resources, the identity, the webhook configuration, and the queries to work with the data. Everything is copy-paste ready — replace the <angle-bracket> placeholders with your own values.

What you will build

Prerequisites
- An Azure subscription with an existing Log Analytics workspace (Sentinel-enabled or plain Log Analytics both work).
- Permission to deploy resources into a resource group, create a Microsoft Entra app registration, and assign Azure roles.
- Azure CLI. Azure Cloud Shell works well; all commands below are single-line and run in PowerShell or bash.
- A Netskope AI SecOps tenant with the Outbound Webhooks feature.
Values you will collect along the way
| Value | Where you get it | Used in |
| <workspace-name> | Your existing Log Analytics workspace | Step 1 |
| <workspace-resource-group> | Resource group that contains the workspace | Step 1 |
| <resource-group> | Resource group for the DCE and DCR (may be the same) | Steps 1–2 |
| <dce-ingestion-endpoint> | Step 1 deployment output | Step 4 |
| <dcr-immutable-id> | Step 1 deployment output | Step 4 |
| <dcr-resource-id> | Step 1 deployment output | Step 2 |
| <tenant-id> | Your Entra tenant ID — Step 2 (az account show) | Step 4 (Token URL) |
| <client-id> | Your app registration's Application (client) ID — Step 2 | Steps 2 and 4 |
| <client-secret> | Step 2 credential creation — shown once | Step 4 |
Step 1 — Deploy the table, DCE, and DCR
Save the ARM template below as netskope-aisecops-dce-dcr.json. It creates three resources: the custom table NetskopeAISecOps_CL inside your workspace, a Data Collection Endpoint, and a Data Collection Rule wired to both. The workspace may live in a different resource group than the DCE and DCR; the template asks for both values. The DCE and DCR must be created in the workspace’s region — the deploy snippet below fetches it with one command and passes it in. The deployment is idempotent — re-running it updates in place.
Deploy it. If you omit --parameters, the CLI prompts for the workspace name and its resource group:
https://github.com/netskopeoss/Netskope_Web_Transactions_Azure_Sentinel/blob/main/Netskope-AI-SecOps-AzureSentinel/netskope-aisecops-dce-dcr.json
$loc = az monitor log-analytics workspace show --resource-group <workspace-resource-group> --workspace-name <workspace-name> --query location -o tsv
az deployment group create --resource-group <resource-group> --name aisecops-webhook --template-file netskope-aisecops-dce-dcr.json --parameters workspaceName=<workspace-name> workspaceResourceGroup=<workspace-resource-group> location=$loc
az deployment group show --resource-group <resource-group> --name aisecops-webhook --query properties.outputsRecord four outputs: dceLogsIngestionEndpoint, dcrImmutableId, dcrResourceIdForRoleAssignment, and destinationUrl (the last is useful for the optional curl test in Step 5).
Step 2 — Create the Microsoft Entra identity
The webhook authenticates with OAuth2 client credentials. Create an app registration, generate a secret, and grant it write access on the DCR:
az ad app create --display-name "netskope-aisecops-webhook" --query appId -o tsv
az ad sp create --id <client-id>
az ad app credential reset --id <client-id> --append --years 1
az role assignment create --assignee <client-id> --role "Monitoring Metrics Publisher" --scope <dcr-resource-id>
az account show --query tenantId -o tsv
Four things to know:
- The first command prints the Application (client) ID — that is your <client-id>. It is unique to the app you just created in your own Entra tenant. There is no shared or Netskope-provided client ID for this integration; values copied from another environment or from screenshots will not work in yours.
- The credential reset output shows the client secret once in its password field. Copy the secret Value into a secret store immediately. Do not confuse it with the Secret ID: the Value contains special characters like ~ and dots and is what authenticates; the Secret ID is a plain GUID used only for managing the credential.
- The Monitoring Metrics Publisher role sits on the DCR itself, so a single assignment covers the integration. If the role command returns "principal not found", the service principal has not replicated yet — wait a minute and re-run just that command.
- The last command prints your Entra tenant ID. You will use it in Step 4 to build the Token URL: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token. Like the client ID, it is specific to your tenant.
Portal alternative: Microsoft Entra ID → App registrations → New registration, then Certificates & secrets → New client secret. Copy the Value column right after creation.
Step 3 — Create the parser function
Save the query below as NetskopeAISecOpsEvents.kql and register it as a workspace function:
https://github.com/netskopeoss/Netskope_Web_Transactions_Azure_Sentinel/blob/main/Netskope-AI-SecOps-AzureSentinel/NetskopeAISecOpsEvents.kql
$parser = Get-Content ./NetskopeAISecOpsEvents.kql -Raw
az monitor log-analytics workspace saved-search create --resource-group <workspace-resource-group> --workspace-name <workspace-name> --name NetskopeAISecOpsEvents --category Netskope --display-name "NetskopeAISecOpsEvents" --saved-query $parser --func-alias NetskopeAISecOpsEventsNetskopeAISecOps_CL is the table — where the data is stored.
Step 4 — Configure the webhook in AI SecOps
In your Netskope tenant, open AI SecOps → Configuration → Integrations → Outbound webhooks and create a webhook.
Connection tab
| Field | Value |
| Name | Any label, for example Azure Sentinel |
| Destination URL | <dce-ingestion-endpoint> — the HOST ONLY, e.g. https://dce-netskope-aisecops-xxxx.eastus-1.ingest.monitor.azure.com. Do not append the path or query string; the webhook builds the full URL from the Payload tab fields |
| Auth type | OAuth2 client credentials |
| Token URL | https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token — built with your own Entra tenant ID from Step 2 |
| Client ID | <client-id> — the Application (client) ID of the app you created in Step 2 |
| Client secret | <client-secret> — the Value, not the Secret ID |
| Scope | https://monitor.azure.com/.default — required; an empty Scope fails the token request |
| Audience / resource | Leave empty |
| Custom headers | None |
| Verify TLS certificate | On. Leave the Custom CA bundle empty — Azure ingestion endpoints present publicly trusted certificates |

Azure Government note: use https://login.microsoftonline.us/<tenant-id>/oauth2/v2.0/token as the Token URL and https://monitor.azure.us/.default as the Scope.
Events tab
Select the event types to deliver. All Cases, User Risk, and AI Risk types can share this single webhook — the table and parser handle every type.

Payload tab
| Field | Value |
| Included data fields | Core plus any optional groups you want (Case metadata, Investigation, Entities, File lineage, Risk detail). Note the PII markers — enable groups per your data-handling policy |
| Wrapping mode | Array (batch-shaped) — the Logs Ingestion API requires a JSON array body |
| DCR immutable ID | <dcr-immutable-id> — from the Step 1 outputs |
| DCR stream name | Custom-NetskopeAISecOps_CL |
| Static fields | None required |
| Severity mapping | Keep the defaults unless your workflow needs different labels |
| Content type | application/json — Compress body (gzip) off |
| Custom body template | Off |

Review & test tab

Step 5 — Test and verify
From Review & test, pick a real event type such as case.created and send a test. Prefer real event types over webhook.test: the synthetic test event carries no business fields and lands as a mostly empty row.
Allow a few minutes. Two timing behaviors are normal on a fresh setup: the first records into a new table take longer to appear, and DCR configuration changes take several minutes to reach the ingestion pipeline. Then query:
NetskopeAISecOps_CL
How the data is structured
Each webhook delivery is a JSON array. Each element is an envelope with delivery metadata at the top level and the actual event under data:
[
{
"id": "evt_...",
"timestamp": "2026-07-02T19:30:00Z",
"tenant_id": "1234",
"region": "us-west-2",
"api_version": "2026-08-01",
"type": "case.created",
"data": { "case_id": "case_01HXYZ", "severity": "high", "...": "..." }
}
]
The table stores the envelope in five typed columns and the event whole in Data:
| Column | Type | Source |
| TimeGenerated | datetime | envelope timestamp (delivery time) |
| EventType | string | envelope type |
| NetskopeTenantId | string | envelope tenant_id |
| Region | string | envelope region |
| ApiVersion | string | envelope api_version |
| Data | dynamic | envelope data — the complete event |
| Group | Columns |
| Cases — core | CaseId, EventTitle, Severity, RuleIds*, Url, User, EntityType, CreatedAt, Status, OldStatus, NewStatus, ChangedBy, Assignee, AssignedBy, InvestigationError, AlertCount, IncidentCount, DataSources*, AssignedTo, GroupId, ViewId, CreationSource, ClosureRecommendation |
| Cases — investigation | Verdict, Summary, SummaryMarkdown, RiskScore, RiskFactors*, RecommendedActions*, DataConfidence*, UserContext*, DataExposure*, IncidentsSummary*, Entities*, KeyEvidence*, FileLineage* |
| Risk — identity | UserId, UserName, AppId, AppName, McpServerId, McpServerName |
| Risk — scoring | Score, Band, PreviousBand, PreviousUci, RiskNote, RiskIndicators*, PrimaryDriver, CategorySubscores*, ScoredAt |
Columns marked * stay dynamic. Reach inside them with dot notation: FileLineage.files[0].md5, DataConfidence.score, RiskIndicators.policy_alerts. Fields not present on a given event type are null — filter with isnotempty().
Troubleshooting
| Symptom | Likely cause | Fix |
| Connection test fails with HTTP 400 from the token endpoint | Scope empty, a value in Audience/resource, or a v1 token URL (/oauth2/token) | Scope https://monitor.azure.com/.default, Audience empty, Token URL ending /oauth2/v2.0/token; save before retesting |
| AADSTS7000215: Invalid client secret | Secret ID pasted instead of the secret Value, or an expired secret | Use the Value (contains ~ and dots); create a new secret if the Value was lost |
| Ingestion returns InvalidApiVersion | Full ingestion URL pasted into Destination URL | Destination URL must be the host only; the DCR immutable ID and stream name belong in the Payload tab |
| Ingestion returns 401 or 403 | Wrong credentials, or the Monitoring Metrics Publisher role is missing or not yet propagated | Verify with az role assignment list --assignee <client-id> --scope <dcr-resource-id>; allow a few minutes after assigning |
| Deployment fails with a not-found error for the workspace | Wrong workspaceName or workspaceResourceGroup parameter | Verify with az monitor log-analytics workspace show --resource-group <rg> --workspace-name <name> and re-run |
| Rows appear but business columns are null | Deliveries processed while a DCR change was still propagating, or webhook.test events | Wait a few minutes and resend a real event type; filter noise with where EventType != "webhook.test" |
| Test returned success but no row appears | Ingestion latency, or the time picker excludes the event time | Wait 5–10 minutes; query with TimeGenerated > ago(1h) and widen the picker |
| InvalidTransformQuery when customizing the DCR | The transform compiler supports a restricted KQL subset | Keep the DCR transform rename-only and do all field extraction in the saved function |
Maintenance
The client secret expires on the date set at creation (one year in the commands above). Rotate it before expiry: create a new secret with az ad app credential reset --id <client-id> --append --years 1, update the webhook’s Client secret field, verify a test delivery, then remove the old credential in Entra. An expired secret stops deliveries without any error visible on the Azure side.



