ai-gebruik-in-beeld

Draaiboek om AI-gebruik in je organisatie feitelijk te meten in plaats van te vermoeden.

Status: in gebruik. Draait als leesbare site en is in gebruik.

AI-gebruik in je organisatie feitelijk meten: welke bronnen er zijn, wat elke bron wel en niet bewijst, en hoe je de meting auditbaar vastlegt. Voor de CISO en de functionaris gegevensbescherming samen.

Bijna elke publieke organisatie stelt nu AI-beleid vast, en vrijwel geen enkele weet op dat moment wat er feitelijk gebeurt. Beleid zonder feitenbeeld is een aanname. Dit draaiboek sluit dat gat: niet "hebben we AI-beleid?" maar "wat draait er, langs welke route, en kunnen we dat aantonen?"

De methode is dezelfde als in de bredere Security Commons-lijn: meet voordat je ingrijpt, vertrouw op data en niet op tekeningen, en "aangezet" is niet "beheerd". De bevindingen komen uit telemetrie en configuratie, niet uit een vragenlijst.

Voor wie

CISO's, privacy officers en informatiemanagers bij publieke organisaties.

Snel starten

Lees het draaiboek. De query's staan er los naast, om aan te passen op je eigen omgeving.

Bijdragen

Zie de CONTRIBUTING van de organisatie: daar staat per project een formulier, ook zonder Git-ervaring.

Licentie

EUPL-1.2, zie LICENSE.

Wat erin zit

Belangrijk vooraf

Verkeersgegevens van medewerkers zijn persoonsgegevens, ook geaggregeerd. Dit onderzoek is geen jacht op individuele gebruikers en geen prestatiemeting. De FG is mede-eigenaar vooraf, niet reviewer achteraf. Toets het eigen IT-reglement, de BIO 2.0 en een eventueel medezeggenschapstraject voordat je de eerste query draait. Het draaiboek werkt dat uit.

Herkomst

Dit draaiboek is afgesplitst van Meten voordat je ingrijpt, waar het als hoofdstuk begon maar uitgroeide tot een zelfstandig product met een eigen grondslag, bewijslast en query-set. Dat stuk blijft over de ClickFix-aanvalsvorm en het verhogen van de security posture; dit gaat specifiek over zicht op AI-gebruik.

Open source onder EUPL v1.2.

08

AI-gebruik in beeld brengen

Bijna elke organisatie stelt nu AI-beleid vast, en vrijwel geen enkele weet op dat moment wat er feitelijk gebeurt. Dit hoofdstuk is het draaiboek voor dat feitenbeeld: welke bronnen er zijn, wat elke bron wel en niet bewijst, en hoe je de meting auditbaar vastlegt. Voor de CISO en de functionaris gegevensbescherming samen, want zonder die tweede handtekening is dit onderzoek niet verdedigbaar.

Gegeneraliseerd uit een concrete gemeentelijke casus. Alle query's staan in queries/; pas hostlijsten, procesnamen en domeinen aan op je eigen omgeving.

A. Vier dingen vastleggen voordat je meet

  1. Twee vragen, twee regimes. Actief gebruik door medewerkers is een personeelsvraag (IT-reglement, medezeggenschap, proportionaliteit). Ingebedde of default-on AI die de leverancier zelf aanzet is een contractvraag (verwerkersovereenkomst, DPIA, leveranciersbeheer). Andere detectie, andere grondslag. Het tweede regime wordt bijna altijd vergeten en levert vaak de scherpste bevindingen op.
  2. Het aggregatieniveau. Fase 1 geaggregeerd: per dienst, per apparaataantal, hooguit per organisatieonderdeel met een ondergrens. Geen individuele accounts in het resultaat. Zodra je individuen kunt aanwijzen verandert de aard van het onderzoek en horen daar aparte waarborgen bij.
  3. De rollen. De FG is mede-eigenaar vooraf, geen reviewer achteraf. Informeer de medezeggenschap, ook als formeel advies niet nodig is. Kondig de meting aan bij de beheerders wier omgeving je bevraagt.
  4. Wat het onderzoek niet is. Geen jacht op individuele overtreders, geen prestatiemeting, geen incidentonderzoek. Zet die zin in de aankondiging en herhaal hem in de rapportage.

Verkeersgegevens van medewerkers zijn persoonsgegevens, ook geaggregeerd. Toets het eigen IT-reglement, de BIO2 en een eventueel medezeggenschapstraject voordat je de eerste query draait.

B. Rechten en bronnen

Alles in dit hoofdstuk is read-only. Geen enkele stap vraagt schrijfrechten, en dat maakt de aanvraag bij de eigen IT-organisatie haalbaar.

Rol Waarvoor
Security Reader of Defender XDR-leesrol Advanced Hunting op de device-tabellen
Global Reader Enterprise applications, consent, accounts als noemer
Purview: View-Only Audit Logs, Activity Explorer Classificatie- en DLP-stand
Leesrechten beheerportaal Geïntegreerde apps, licenties, agents, organisatie-instellingen

De bronnen liggen in drie lagen. Elke laag levert zelfstandig een rapporteerbaar resultaat, dus er is geen licentieniveau waar dit onderzoek op staat of valt.

Laag Bron Drempel Levert
0. Beheer en configuratie Beheerportaal, identiteitsbeheer Leesrechten, geen extra licentie Governance-feiten, direct rapporteerbaar
1. Endpointtelemetrie Advanced Hunting: netwerk, processen, bestanden, software-inventaris Endpointbescherming in het plan waar Advanced Hunting in zit Gebruiksbeeld: welke diensten, hoeveel apparaten
2. Cloud-app- en datalaag Cloud-app-security, classificatie en DLP, postvaktoegang Aanvullende modules of hoger plan Verdieping en dienstzijdige aantallen

Heb je laag 1 of 2 niet, dan blijft het onderzoek uitvoerbaar: proxy-, firewall- of DNS-logging beantwoordt dezelfde vraag (hoofdstuk 04), laag 0 levert al een compleet governance-hoofdstuk, en je kunt de meting bij je werkplekleverancier beleggen. Dat laatste is meteen een regietoets.

C. Vier blokker-checks vooraf (een kwartier)

Wie deze overslaat, ontdekt na drie dagen dat een deel niet kon. Draai ze eerst en noteer de uitkomst.

  1. Kan ik Advanced Hunting draaien, en op welk account? Sommige tabellen lossen niet op vanaf een gewoon account. Test met ai-probe-bronnen.kql.
  2. Wat is de bron van de cloud-app-cijfers? Als die uit een andere integratie komen dan je endpointtelemetrie, meet je twee verschillende populaties.
  3. Bestaat endpoint-DLP? Zo niet, dan vervalt de hele datalaag en is dat zelf de bevinding.
  4. Wat is mijn noemer? Aantal beheerde apparaten en actieve accounts, uit dezelfde populatie als je teller. Zonder noemer is elk absoluut getal stuurloos.

D. De bewijslast per vraag

De laatste kolom is de belangrijkste: die beschermt je tegen een lezer die meer in je cijfer leest dan erin zit.

Vraag Bron Draagt Draagt niet
Welke AI-diensten, hoe breed? Netwerkgebeurtenissen Aantal apparaten per dienst, verhoudingen Wie, hoe vaak per persoon, wat ingevoerd is
Via welke software? Netwerk plus proces Browser, desktop-app, CLI, kantoorsoftware Of er bedrijfsdata in gaat
Draait er AI-software lokaal? Software-inventaris, procesgebeurtenissen Wat geïnstalleerd is en waar Of het gebruikt wordt
Gaat er inhoud naar buiten? Verkeer naar bestands- en uploadhosts Dat er bestandsverkeer is Welke bestanden, welke gevoeligheid
Programmatisch of via de website? Verkeer naar API-endpoints Onderscheid casual gebruik en integratie Wat de integratie doet
Zit AI in browserextensies? Extensie-inventaris, anders bestandssporen Welke extensies aanwezig zijn Wat de extensie met pagina-inhoud doet
Leest een assistent mee in de mail? Postvaktoegang per applicatie Welke applicaties postvakken benaderen De inhoud van wat gelezen is
Kan een gebruiker zelf AI toevoegen? Beheerportaal: apps, zelfbediening Hard governance-feit Niets over feitelijk gebruik
Bestaan er AI-agents, wie beheert ze? Beheerportaal: agentoverzicht Aantal, herkomst, eigenaarloosheid Het risico per agent
Wat zette de leverancier zelf aan? Detailscherm per agent of dienst Dat activering buiten de organisatie om ging Of er data mee verwerkt is
Hoeveel AI is ingekocht? Beheerportaal: licenties Verschil tussen betaald en gebundeld Wie ze gebruikt
Ligt er classificatie en DLP onder? Purview Het fundament onder elke datauitspraak Of er data is weggelekt

E. Laag 0: de portaalchecks

Een dagdeel klikwerk, en het deel van de rapportage dat het snelst tot besluiten leidt. Noteer per check het menupad en de datum, want paden wijzigen.

Check Wat je noteert Waarom het telt
Geïntegreerde apps en add-instore Aantal uitgerold en aantal installeerbaar Open store met lege blokkeerlijst betekent dat iedereen zelf AI kan toevoegen
Zelfbediening apps en licenties Aan of uit Verklaart doorgaans wat je bij de vorige check aantreft
AI-agents Aantal, gesplitst naar eigen bouw en leverancier Schaduw-automatisering die in geen enkel register staat
Agents zonder eigenaar Aantal en herkomst Eigenaarloos betekent onbeheerd, ongeacht het aantal
Herkomst leverancier-agents Wie activeerde, blijkens het detailscherm "Niemand, de leverancier deed het" is een contractbevinding
Licenties Betaald tegenover gebundeld Voorkomt de conclusie dat massaal is ingekocht wat gewoon aanstond
Consent en applicatietoegang Apps met verstrekkende rechten, wie consent mag geven De directe route naar tenantdata
Classificatie en DLP Labels, endpoint-DLP, koppeling Zonder dit is elke uitspraak over gevoelige data een aanname
Noemer Actieve accounts, beheerde apparaten, geblokkeerd deel De deler voor elk percentage

F. De query's

Vijftien query's in queries/, met per bestand het doel, de vereisten en de bewuste beperkingen in de kop. Draai ze in deze volgorde:

  1. Fundament. ai-probe-bronnen en ai-noemer-apparaten: wat is haalbaar, en wat is mijn deler.
  2. Basisbeeld. ai-diensten-netwerk (welke diensten, hoeveel apparaten), ai-diensten-per-proces (via welke software), ai-contentverkeer (gaat er inhoud heen), ai-directe-api (integratie of website), ai-software-inventaris (wat staat geinstalleerd).
  3. Ingebedde AI. ai-ingebedde-assistent-schoon en ai-assistent-interacties scheiden de assistent van je eigen platform van gebruikersgedrag; ai-office-apps-naar-ai zoekt add-ins in kantoorsoftware.
  4. Losse bronnen. ai-browserextensies en ai-mailtoegang-apps.
  5. Splitsing. ai-organisatie-splitsing voor een gedeelde tenant, ai-afdelingsclustering voor adoptie per onderdeel met een ondergrens. Die tweede rekent tussentijds op accountniveau: alleen draaien met een afspraak met de FG.
  6. Controle. ai-volledigheidssweep over circa zestig diensten: heb je iets gemist.

G. Leg elke run vast

Een getal zonder herkomst is een mening. Nummer query's en portaalchecks in aparte reeksen en noteer de kanttekening op het moment zelf; achteraf reconstrueren lukt zelden.

Nr Datum Account Vraag Methode Uitkomst Export Kanttekening
R1 normaal of beheer Welke diensten? Query, 30 dagen verwijzing bestandsnaam met datum venster, filters, uitsluitingen
C1 Add-instore open? Portaalcheck met menupad getal schermafdruk pad kan gewijzigd zijn

Dit maakt de meting herhaalbaar, overdraagbaar en verdedigbaar, en het dwingt tot eerlijkheid over mislukkingen. Een query die omviel of een duiding die je moest terugnemen hoort er net zo goed in als een treffer.

H. Valkuilen

I. Wat niet werkt, en dat scheelt je een dag

De verleiding is groot om via de dataclassificatietabellen te willen aantonen dat er gevoelige bestanden naar AI gaan. Dat lukt niet: gebeurtenissen over gevoelige bestandstoegang zijn zonder classificatiefundament niet interpreteerbaar, en het proces dat een bestand leest zegt niets over waar dat bestand daarna heen gaat. Wat er wel is: het uploadsignaal uit ai-contentverkeer.kql als indicatie, en de vaststelling dat er geen classificatie ligt als bevinding op zichzelf. Die tweede is bestuurlijk sterker dan een halfbakken cijfer.

J. Rapporteren

Zet vier beperkingen in hetzelfde hoofdstuk als de methode, niet in een voetnoot: het meetvenster is een momentopname, de populatie is het beheerde deel (onbeheerde apparaten leveren niets), de inhoud is niet gemeten, en er is geen individuele attributie. Dat laatste is een kwaliteit, geen tekortkoming.

Zet de bevindingen daarna in het IST naar SOLL-sjabloon uit hoofdstuk 01, met een actiehouder per regel.

Bevinding Richting
Brede publieke AI in gebruik, geen alternatief aangewezen Wijs eerst een voorziening aan, blokkeer daarna. Blokkeren zonder alternatief verplaatst het gebruik naar privéapparatuur en maakt het onzichtbaar
App- of add-instore open Blokkeerlijst inrichten, zelfbediening heroverwegen, eigenaar beleggen
Agents zonder eigenaar of door de leverancier geactiveerd Eigenaarschap beleggen, activering agenderen als contract- en releasevraag
Geen classificatie of DLP Fundamentkeuze op directieniveau; dit blokkeert vrijwel elke verdere AI-maatregel
Geen AI-register of aangewezen diensten in het beleid Zolang niets is aangewezen is niets geautoriseerd. Vermijd het woord "toegestaan" en schrijf op wat er feitelijk staat
Advies

rapporteer de governance-bevindingen uit laag 0 vóór de volumecijfers uit laag 1. Ze zijn harder, ze zijn direct te herstellen, en ze verleiden niet tot een discussie over de precisie van een getal. Plan meteen de tweede ronde: twee metingen met dezelfde methode zijn een trend, twee met verschillende methoden zijn twee momentopnamen die elkaar tegenspreken.

KQL

Herbruikbare query's

Voor Microsoft Defender Advanced Hunting. Pas parent-processen en uitsluitingen aan op je eigen omgeving; de toelichting staat in de commentaarregels van elke query.

ai-probe-bronnen.kql

vier probes van een minuut die vaststellen welk deel van het onderzoek in jouw omgeving haalbaar is.

Toon query
// ai-probe-bronnen.kql
// Doel: vier probes van een minuut die vaststellen welk deel van het onderzoek in jouw omgeving haalbaar is.
// Vereist: Advanced Hunting. Draai ze vanaf het account waarmee je het onderzoek gaat doen.
// Beperking: dit meet niets, het toetst je bronnen. Een lege of ontbrekende bron is zelf een bevinding en
//   hoort als zodanig in de run-log, niet als "geen AI aangetroffen".
// Draai de blokken los van elkaar; ze staan hier bij elkaar omdat ze samen een kwartier kosten.

// --- 1. Is er uberhaupt AI-verkeer zichtbaar, en lost de tabel op vanaf dit account? -------------------
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where RemoteUrl has_any ("openai.com", "chatgpt.com", "claude.ai", "anthropic.com",
                           "gemini.google.com", "copilot.microsoft.com", "perplexity.ai")
| summarize Verbindingen = count(), Apparaten = dcount(DeviceId)

// --- 2. Bestaat de cloud-app-tabel, en wat zit erin? ---------------------------------------------------
// Lost deze niet op ("Failed to resolve table"), probeer dan het beheeraccount voordat je concludeert
// dat de bron ontbreekt.
CloudAppEvents
| where Timestamp > ago(7d)
| summarize Events = count() by Application
| sort by Events desc

// --- 3. Is de UPN-kolom gevuld? Bepaalt of splitsen per organisatie mogelijk is ------------------------
DeviceNetworkEvents
| where Timestamp > ago(1d)
| summarize Totaal = count(), MetUpn = countif(isnotempty(InitiatingProcessAccountUpn))
| extend VulgraadPct = round(100.0 * MetUpn / Totaal, 1)

// --- 4. Bestaat het afdelingsattribuut, en is het gevuld? Bepaalt of clustering mogelijk is ------------
IdentityInfo
| where isnotempty(AccountUpn)
| extend Org = tolower(tostring(split(AccountUpn, "@")[1]))
| where Org == "eigen-domein.nl"   // vul je eigen e-maildomein in
| summarize Accounts = count(), MetAfdeling = countif(isnotempty(Department))
| extend VulgraadPct = round(100.0 * MetAfdeling / Accounts, 1)

// --- 5. Bestaat endpoint-DLP? Bepaalt of de datalaag uberhaupt iets kan opleveren ---------------------
DeviceEvents
| where Timestamp > ago(7d)
| summarize Events = count() by ActionType
| sort by Events desc
| take 100

ai-noemer-apparaten.kql

de noemer vaststellen: hoeveel apparaten zie je eigenlijk, uitgesplitst naar platform en onboardingstatus. Zonder noemer is elk absoluut getal in een rapportage stuurloos.

Toon query
// ai-noemer-apparaten.kql
// Doel: de noemer vaststellen: hoeveel apparaten zie je eigenlijk, uitgesplitst naar platform en
//   onboardingstatus. Zonder noemer is elk absoluut getal in een rapportage stuurloos.
// Vereist: Advanced Hunting (DeviceInfo).
// Beperking: dit is de beheerde vloot. Onbeheerde apparaten leveren geen telemetrie en vallen dus buiten
//   elke meting in dit hoofdstuk; benoem die populatiegrens expliciet in de rapportage.
// Let op: zorg dat teller en noemer over dezelfde populatie gaan. Het aantal actieve accounts uit het
//   beheerportaal is een andere noemer dan het aantal apparaten hier.

DeviceInfo
| where Timestamp > ago(30d)
| summarize arg_max(Timestamp, OnboardingStatus, DeviceType, OSPlatform) by DeviceId
| summarize Apparaten = dcount(DeviceId) by OnboardingStatus, DeviceType, OSPlatform
| sort by Apparaten desc

// Totaal in een getal:
// DeviceInfo | where Timestamp > ago(30d) | summarize Apparaten = dcount(DeviceId)

ai-diensten-netwerk.kql

het basisbeeld. Per AI-dienst het aantal apparaten en verbindingen over een vast venster.

Toon query
// ai-diensten-netwerk.kql
// Doel: het basisbeeld. Per AI-dienst het aantal apparaten en verbindingen over een vast venster.
// Vereist: Advanced Hunting (DeviceNetworkEvents).
// Beperking: verbindingen zijn geen gebruikers en zeggen niets over de inhoud. Apparaataantal is de
//   bruikbare breedtemaat; verbindingen hooguit een indicatie van intensiteit.
// Aanpassen: de hostlijst en de indeling in de case(). Houd de lijst bij als eigen artefact, het aanbod
//   verandert per kwartaal. De ingebedde assistent van je eigen platform hoort apart geteld te worden
//   (zie ai-ingebedde-assistent-schoon.kql), niet als shadow-AI.

DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any ("openai.com", "chatgpt.com", "claude.ai", "anthropic.com",
                           "gemini.google.com", "copilot.microsoft.com", "perplexity.ai",
                           "poe.com", "mistral.ai", "deepseek.com", "x.ai", "grok", "huggingface.co")
| extend Tool = case(
    RemoteUrl has_any ("chatgpt.com", "openai.com"), "ChatGPT/OpenAI",
    RemoteUrl has_any ("claude.ai", "anthropic.com"), "Claude/Anthropic",
    RemoteUrl has "perplexity.ai", "Perplexity",
    RemoteUrl has "gemini.google.com", "Gemini",
    RemoteUrl has "copilot.microsoft.com", "Copilot (platform-default)",
    RemoteUrl has "mistral.ai", "Mistral",
    RemoteUrl has_any ("x.ai", "grok"), "Grok/xAI",
    RemoteUrl has "huggingface.co", "HuggingFace",
    RemoteUrl has "deepseek.com", "DeepSeek",
    "Overig")
| summarize Verbindingen = count(), Apparaten = dcount(DeviceId),
            Eerste = min(Timestamp), Laatste = max(Timestamp)
    by Tool
| sort by Apparaten desc

ai-diensten-per-proces.kql

via welke software het AI-verkeer loopt: browser, geinstalleerde desktop-app, uitwijkbrowser, CLI- en ontwikkeltooling, kantoorsoftware of systeemproces.

Toon query
// ai-diensten-per-proces.kql
// Doel: via welke software het AI-verkeer loopt: browser, geinstalleerde desktop-app, uitwijkbrowser,
//   CLI- en ontwikkeltooling, kantoorsoftware of systeemproces.
// Vereist: Advanced Hunting (DeviceNetworkEvents).
// Beperking: het startende proces zegt iets over de route, niet over de gebruiker of de inhoud.
// Lezing: systeemprocessen in deze uitkomst zijn ingebedde AI en geen gebruikersgedrag. Een desktop-app of
//   CLI-tool duidt op bewuste installatie; een uitwijkbrowser duidt op omzeiling van de beheerde browser.

DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any ("openai.com", "chatgpt.com", "claude.ai", "anthropic.com",
                           "gemini.google.com", "copilot.microsoft.com", "perplexity.ai",
                           "poe.com", "mistral.ai", "deepseek.com", "x.ai", "grok")
| extend Tool = case(
    RemoteUrl has_any ("chatgpt.com", "openai.com"), "ChatGPT/OpenAI",
    RemoteUrl has_any ("claude.ai", "anthropic.com"), "Claude/Anthropic",
    RemoteUrl has "perplexity.ai", "Perplexity",
    RemoteUrl has "gemini.google.com", "Gemini",
    RemoteUrl has "copilot.microsoft.com", "Copilot (platform-default)",
    "Overig")
| summarize Verbindingen = count(), Apparaten = dcount(DeviceId) by Tool, InitiatingProcessFileName
| sort by Apparaten desc

ai-contentverkeer.kql

onderscheid tussen "iemand stelde een vraag" en "er ging een bestand heen", door te tellen op de aparte bestands- en uploadhosts van AI-aanbieders.

Toon query
// ai-contentverkeer.kql
// Doel: onderscheid tussen "iemand stelde een vraag" en "er ging een bestand heen", door te tellen op de
//   aparte bestands- en uploadhosts van AI-aanbieders.
// Vereist: Advanced Hunting (DeviceNetworkEvents).
// Beperking: dit toont dat er bestandsverkeer plaatsvindt, niet welke bestanden of welke gegevens. Gebruik
//   het als signaal voor het gesprek over classificatie, nooit als bewijs van een datalek.
// Aanpassen: aanbieders verplaatsen hun bestandsopslag naar andere subdomeinen; controleer de lijst per ronde.

let ContentHosts = dynamic(["oaiusercontent.com", "files.oaiusercontent.com", "cdn.openai.com",
                            "files.anthropic.com"]);
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any (ContentHosts)
| summarize Verbindingen = count(), Apparaten = dcount(DeviceId), Laatste = max(Timestamp)

// Vergelijk deze uitkomst met het apparaataantal van dezelfde dienst uit ai-diensten-netwerk.kql.
// Liggen ze in dezelfde orde van grootte, dan is uploaden gangbaar gedrag en niet de uitzondering
// waar beleid vaak van uitgaat.

ai-directe-api.kql

programmatisch AI-gebruik onderscheiden van gebruik via de website: aanroepen naar API-endpoints, per endpoint en per startend proces.

Toon query
// ai-directe-api.kql
// Doel: programmatisch AI-gebruik onderscheiden van gebruik via de website: aanroepen naar API-endpoints,
//   per endpoint en per startend proces.
// Vereist: Advanced Hunting (DeviceNetworkEvents).
// Beperking: dit toont dat er een integratie is, niet wat die integratie doet of met welke data.
// Lezing: API-verkeer is smal en diep (weinig apparaten, veel volume, vaak ontwikkel- of beheertooling),
//   webverkeer is breed en ondiep. Die twee vragen om verschillende maatregelen: het eerste om afspraken
//   met de bouwer, het tweede om een voorziening en voorlichting.

DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any ("api.openai.com", "api.anthropic.com", "api.perplexity.ai",
                           "api.mistral.ai", "api.x.ai", "api.deepseek.com")
| summarize Verbindingen = count(), Apparaten = dcount(DeviceId)
    by RemoteUrl, InitiatingProcessFileName
| sort by Apparaten desc

ai-software-inventaris.kql

welke AI-software staat geinstalleerd, ongeacht of hij deze maand verkeer genereerde. Andere bron dan netwerk- en procesgebeurtenissen, en daarmee een onafhankelijke controle op het beeld.

Toon query
// ai-software-inventaris.kql
// Doel: welke AI-software staat geinstalleerd, ongeacht of hij deze maand verkeer genereerde. Andere bron
//   dan netwerk- en procesgebeurtenissen, en daarmee een onafhankelijke controle op het beeld.
// Vereist: Advanced Hunting met kwetsbaarhedenbeheer (DeviceTvmSoftwareInventory).
// Beperking: geinstalleerd is niet gebruikt, en de inventaris kent alleen software die als product wordt
//   herkend. Losse uitvoerbare bestanden en portable tooling staan er niet in.
// Aanpassen: de zoektermen. Voeg toe wat in jouw omgeving speelt, en houd er rekening mee dat brede termen
//   ("notion", "copilot") ook niet-AI-producten raken; controleer de uitkomst op naam en uitgever.

DeviceTvmSoftwareInventory
| where SoftwareName has_any ("claude", "copilot", "cursor", "ollama", "lm studio", "lmstudio",
                              "perplexity", "comet", "deepl", "whisper", "codex", "chatgpt")
| summarize Apparaten = dcount(DeviceId) by SoftwareVendor, SoftwareName
| sort by Apparaten desc

ai-ingebedde-assistent-schoon.kql

de ingebedde assistent van je eigen platform apart tellen, met de systeemruis eruit. Zonder deze schoonmaak rapporteer je een AI-adoptie die niemand heeft uitgevoerd.

Toon query
// ai-ingebedde-assistent-schoon.kql
// Doel: de ingebedde assistent van je eigen platform apart tellen, met de systeemruis eruit. Zonder deze
//   schoonmaak rapporteer je een AI-adoptie die niemand heeft uitgevoerd.
// Vereist: Advanced Hunting (DeviceNetworkEvents).
// Beperking: ook na filtering telt dit verbindingen, niet interacties. Voor echte interacties is de
//   dienstzijdige bron nodig (zie ai-assistent-interacties.kql); die twee getallen verschillen fors.
// Aanpassen: de uitsluitingslijst. Achtergrondprocessen, brokers en webview-componenten van het platform
//   praten uit zichzelf met de assistentdienst en horen niet in een gebruikscijfer.

DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has "copilot.microsoft.com"
| where InitiatingProcessFileName !in~ (
    "backgroundtaskhost.exe", "lsass.exe", "svchost.exe",
    "microsoft.aad.brokerplugin.exe", "onedrive.exe", "msedgewebview2.exe")
| summarize Verbindingen = count(), Apparaten = dcount(DeviceId) by InitiatingProcessFileName
| sort by Apparaten desc

// Schoon totaal in een getal (zelfde filter, zonder uitsplitsing):
// ... | summarize Verbindingen = count(), Apparaten = dcount(DeviceId)

ai-assistent-interacties.kql

echte interacties met de ingebedde assistent, dienstzijdig gemeten in plaats van via netwerkverbindingen. Dit is het getal dat je in een rapportage wilt hebben.

Toon query
// ai-assistent-interacties.kql
// Doel: echte interacties met de ingebedde assistent, dienstzijdig gemeten in plaats van via
//   netwerkverbindingen. Dit is het getal dat je in een rapportage wilt hebben.
// Vereist: cloud-app-telemetrie in Advanced Hunting (CloudAppEvents).
// Beperking: de applicatienaam verschilt per tenant en per productversie; controleer eerst met probe 2 uit
//   ai-probe-bronnen.kql welke namen in jouw omgeving voorkomen en pas de filterregel aan.
// Lezing: leg het gebruikersaantal hiernaast het aantal betaalde licenties uit het beheerportaal. Is het
//   gebruikersaantal veel groter, dan gebruikt men de gratis gebundelde variant. Dat verandert de
//   contractvraag en meestal ook de dataverwerkingsvraag.

CloudAppEvents
| where Timestamp > ago(30d)
| where Application has "Copilot"
| summarize Interacties = count(),
            Gebruikers = dcount(AccountId),
            Dagen_actief = dcount(bin(Timestamp, 1d)),
            Eerste = min(Timestamp),
            Laatste = max(Timestamp)
    by Application
| sort by Interacties desc

ai-office-apps-naar-ai.kql

kantoorapplicaties die zelf AI-diensten aanroepen, als indicatie van add-ins.

Toon query
// ai-office-apps-naar-ai.kql
// Doel: kantoorapplicaties die zelf AI-diensten aanroepen, als indicatie van add-ins.
// Vereist: Advanced Hunting (DeviceNetworkEvents).
// Beperking, en dit is de belangrijkste van dit hoofdstuk: verkeer vanuit de mailclient is grotendeels
//   trackingpixels uit nieuwsbrieven en marketingmail, niet een AI-add-in. Tel de mailclient dus nooit mee
//   zonder controle. De harde kern zit meestal bij de tekstverwerker en de presentatiesoftware, en is klein.
// Vervolgstap: draai de tweede variant hieronder om het bovenliggende proces te zien. Een webview-component
//   als ouder duidt op een add-in binnen de applicatie; de applicatie zelf als ouder duidt op de gebruiker.

DeviceNetworkEvents
| where Timestamp > ago(30d)
| where InitiatingProcessFileName in~ ("outlook.exe", "winword.exe", "excel.exe", "powerpnt.exe", "onenote.exe")
| where RemoteUrl has_any ("openai.com", "chatgpt.com", "oaiusercontent.com", "anthropic.com", "claude.ai",
                           "perplexity.ai", "mistral.ai", "x.ai", "deepseek.com")
| summarize Verbindingen = count(), Apparaten = dcount(DeviceId) by InitiatingProcessFileName, RemoteUrl
| sort by Apparaten desc, Verbindingen desc

// Variant met het bovenliggende proces erbij:
// DeviceNetworkEvents
// | where Timestamp > ago(30d)
// | where RemoteUrl has_any ("chatgpt.com", "openai.com")
// | where InitiatingProcessFileName in~ ("winword.exe", "powerpnt.exe", "excel.exe", "outlook.exe", "msedgewebview2.exe")
// | summarize Verbindingen = count(), Apparaten = dcount(DeviceId)
//     by InitiatingProcessFileName, InitiatingProcessParentFileName
// | sort by Apparaten desc

ai-browserextensies.kql

browserextensies in beeld brengen als de extensie-inventaris in het portaal leeg blijft, door extensie-ID's af te leiden uit bestandssporen op de werkplek.

Toon query
// ai-browserextensies.kql
// Doel: browserextensies in beeld brengen als de extensie-inventaris in het portaal leeg blijft, door
//   extensie-ID's af te leiden uit bestandssporen op de werkplek.
// Vereist: Advanced Hunting (DeviceFileEvents).
// Beperking: dit levert ID's, geen namen en geen gedrag. Een lege inventaris in het portaal is trouwens
//   vaak geen rechtenprobleem maar een licentiegat; check dat eerst, die route is korter.
// Vervolgstap: zoek elk ID op in een openbare extensiecatalogus, noteer naam, uitgever en gevraagde rechten,
//   en leg die lookup vast als aparte stap in de run-log. Een AI-extensie die meeleest ziet alles wat de
//   gebruiker ziet, dus de gevraagde rechten zijn hier belangrijker dan de naam.

DeviceFileEvents
| where Timestamp > ago(30d)
| where FolderPath contains @"\Extensions\"
| extend ExtId = extract(@"\\Extensions\\([a-p]{32})", 1, FolderPath)
| where isnotempty(ExtId)
| summarize Apparaten = dcount(DeviceId), Bestanden = count(), Laatste = max(Timestamp) by ExtId
| sort by Apparaten desc

ai-mailtoegang-apps.kql

vaststellen of er een AI-assistent in postvakken meeleest, door postvaktoegang te groeperen per applicatie-identiteit in plaats van per gebruiker.

Toon query
// ai-mailtoegang-apps.kql
// Doel: vaststellen of er een AI-assistent in postvakken meeleest, door postvaktoegang te groeperen per
//   applicatie-identiteit in plaats van per gebruiker.
// Vereist: cloud-app-telemetrie in Advanced Hunting (CloudAppEvents met MailItemsAccessed).
// Beperking: dit toont welke applicaties postvakken benaderen, niet wat er is gelezen. De filter op REST
//   beperkt de uitkomst tot programmatische toegang; dat is precies waar een assistent zou zitten.
// Vervolgstap: leg de gevonden applicatie-identiteiten naast de toegestane applicaties in je
//   identiteitsbeheer. Onbekende identiteiten zijn een onderzoeksvraag, geen bevinding: controleer ze in de
//   eigen tenant en niet via een zoekmachine, want productnamen lijken op elkaar.

CloudAppEvents
| where Timestamp > ago(30d)
| where ActionType == "MailItemsAccessed"
| extend ClientInfo = tostring(RawEventData.ClientInfoString), AppId = tostring(RawEventData.AppId)
| where ClientInfo has "REST"
| summarize Postvakken = dcount(AccountObjectId), Events = count(), Laatste = max(Timestamp)
    by AppId, ClientInfo
| sort by Postvakken desc

ai-organisatie-splitsing.kql

AI-gebruik splitsen per organisatie in een gedeelde tenant (gemeenschappelijke regeling, shared service center, ambtelijke fusie), zonder individuele accounts in het resultaat.

Toon query
// ai-organisatie-splitsing.kql
// Doel: AI-gebruik splitsen per organisatie in een gedeelde tenant (gemeenschappelijke regeling, shared
//   service center, ambtelijke fusie), zonder individuele accounts in het resultaat.
// Vereist: Advanced Hunting (DeviceNetworkEvents) en een gevulde UPN-kolom; toets dat eerst met probe 3
//   uit ai-probe-bronnen.kql.
// Beperking: de organisatiekaart wijst elk apparaat toe aan de organisatie waarvan het meeste verkeer komt.
//   Gedeelde apparaten worden dus toegewezen aan hun dominante gebruiker. Dat is bruikbaar voor
//   verhoudingen, niet voor exacte aantallen per organisatie.
// De aggregatiegrens: het domeindeel achter de @ komt in het resultaat, losse adressen niet.

let OrgMap =
    DeviceNetworkEvents
    | where Timestamp > ago(30d)
    | extend Org = tolower(tostring(split(InitiatingProcessAccountUpn, "@")[1]))
    | where isnotempty(Org)
    | summarize Events = count() by DeviceId, Org
    | summarize arg_max(Events, Org) by DeviceId
    | project DeviceId, Org;
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any ("openai.com", "chatgpt.com", "claude.ai", "anthropic.com",
                           "gemini.google.com", "copilot.microsoft.com", "perplexity.ai",
                           "poe.com", "mistral.ai", "deepseek.com", "x.ai", "grok", "huggingface.co")
| extend Tool = case(
    RemoteUrl has_any ("chatgpt.com", "openai.com"), "ChatGPT/OpenAI",
    RemoteUrl has_any ("claude.ai", "anthropic.com"), "Claude/Anthropic",
    RemoteUrl has "perplexity.ai", "Perplexity",
    RemoteUrl has "gemini.google.com", "Gemini",
    RemoteUrl has "copilot.microsoft.com", "Copilot (platform-default)",
    RemoteUrl has "mistral.ai", "Mistral",
    RemoteUrl has_any ("x.ai", "grok"), "Grok/xAI",
    RemoteUrl has "huggingface.co", "HuggingFace",
    RemoteUrl has "deepseek.com", "DeepSeek",
    "Overig")
| join kind=inner OrgMap on DeviceId
| summarize Verbindingen = count(), Apparaten = dcount(DeviceId) by Org, Tool
| sort by Org asc, Apparaten desc

// De noemer per organisatie (draai apart, zelfde OrgMap):
// OrgMap | summarize Apparaten = dcount(DeviceId) by Org | sort by Apparaten desc
//
// Hoort een organisatie meerdere e-maildomeinen te omvatten (verbonden partijen, museum, sociale dienst),
// stel die lijst dan vooraf vast en leg de keuze vast in de run-log. Achteraf domeinen toevoegen maakt
// twee meetrondes onvergelijkbaar.

ai-afdelingsclustering.kql

adoptie en uploadaandeel per organisatieonderdeel, met een ondergrens die herleidbaarheid naar personen voorkomt.

Toon query
// ai-afdelingsclustering.kql
// Doel: adoptie en uploadaandeel per organisatieonderdeel, met een ondergrens die herleidbaarheid naar
//   personen voorkomt.
// Vereist: Advanced Hunting (DeviceNetworkEvents, IdentityInfo) en een gevuld afdelingsattribuut; toets dat
//   eerst met probe 4 uit ai-probe-bronnen.kql.
// Beperking, en die is wezenlijk: deze query rekent per account en aggregeert daarna. Tussen die twee
//   stappen bestaat er een resultaat op persoonsniveau. Dat is een grens waar de FG vooraf iets van moet
//   vinden; draai hem niet zonder die afspraak, en exporteer alleen het geaggregeerde eindresultaat.
// De ondergrens: onderdelen met minder dan tien accounts worden samengevoegd tot een restcluster. Verhoog
//   die drempel als je organisatie kleine teams kent.

let AIDomains = dynamic(["openai.com", "chatgpt.com", "claude.ai", "anthropic.com", "gemini.google.com",
                         "perplexity.ai", "mistral.ai", "deepseek.com", "x.ai", "huggingface.co"]);
let EigenDomeinen = dynamic(["organisatie-a.nl", "organisatie-b.nl"]);   // vul je eigen domeinen in
let PerAccount =
    DeviceNetworkEvents
    | where Timestamp > ago(30d)
    | extend Upn = tolower(InitiatingProcessAccountUpn)
    | where isnotempty(Upn)
    | where tostring(split(Upn, "@")[1]) in (EigenDomeinen)
    | summarize ApparatenAI = dcountif(DeviceId, RemoteUrl has_any (AIDomains)),
                Upload      = dcountif(DeviceId, RemoteUrl has "oaiusercontent.com")
      by Upn;
let Afdelingen =
    IdentityInfo
    | where isnotempty(AccountUpn)
    | summarize Department = max(Department) by Upn = tolower(AccountUpn);
PerAccount
| join kind=leftouter Afdelingen on Upn
| extend Afdeling = iff(isempty(Department), "onbekend", Department)
| summarize Accounts = count(),
            AccountsAI = countif(ApparatenAI > 0),
            AccountsUpload = countif(Upload > 0)
    by Afdeling
| extend Cluster = iff(Accounts >= 10, Afdeling, "Overig (clusters onder 10)")
| summarize Accounts = sum(Accounts), AccountsAI = sum(AccountsAI), AccountsUpload = sum(AccountsUpload)
    by Cluster
| extend AdoptiePct = round(100.0 * AccountsAI / Accounts, 1),
         UploadaandeelPct = round(100.0 * AccountsUpload / Accounts, 1)
| sort by AdoptiePct desc

ai-volledigheidssweep.kql

toetsen of je eigen hostlijst niets van betekenis heeft gemist, door breed te zoeken over circa zestig AI-diensten en het resultaat per host te tonen.

Toon query
// ai-volledigheidssweep.kql
// Doel: toetsen of je eigen hostlijst niets van betekenis heeft gemist, door breed te zoeken over circa
//   zestig AI-diensten en het resultaat per host te tonen.
// Vereist: Advanced Hunting (DeviceNetworkEvents).
// Beperking: dit is een controlemiddel, geen meting. Zonder deze stap is "wij vonden geen andere diensten"
//   een uitspraak over je eigen zoeklijst en niet over je omgeving.
// Vervolgstap: alles wat hier bovenkomt en niet in je kernlijst staat, trieer je met de hand voordat het in
//   een rapportage belandt. Vul daarna je kernlijst aan en noteer die wijziging, anders zijn je twee
//   meetrondes niet vergelijkbaar.

let aiDomains = dynamic([
  "chatgpt.com","openai.com","oaiusercontent.com","claude.ai","anthropic.com","gemini.google.com","aistudio.google.com",
  "copilot.microsoft.com","perplexity.ai","poe.com","huggingface.co","mistral.ai","x.ai","grok.com","deepseek.com",
  "character.ai","pi.ai","you.com","phind.com","kagi.com","jasper.ai","copy.ai","writesonic.com","quillbot.com","notion.ai",
  "midjourney.com","leonardo.ai","stability.ai","runwayml.com","elevenlabs.io","heygen.com","synthesia.io","suno.com","udio.com","pika.art",
  "githubcopilot.com","cursor.com","codeium.com","tabnine.com","replit.com","bolt.new","v0.dev","lovable.dev",
  "otter.ai","fireflies.ai","fathom.video","tldv.io","read.ai","notta.ai","chatpdf.com","humata.ai","sider.ai","monica.im",
  "getmerlin.in","deepl.com","gamma.app","tome.app"
]);
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any (aiDomains)
| extend Host = tolower(extract("^(?:https?://)?([^/:]+)", 1, RemoteUrl))
| summarize Apparaten = dcount(DeviceId), Verbindingen = count(), Laatste = max(Timestamp) by Host
| sort by Apparaten desc