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.
draaiboek.md - het volledige draaiboek: wat je vastlegt voordat je meet
(twee regimes, aggregatieniveau, rollen), de bronnen en hun bewijslast, de portaalchecks, en de
auditbare run-log.
queries/ - 15 herbruikbare KQL-query's voor Microsoft Advanced Hunting, van
software-inventaris en netwerkverkeer tot ingebedde assistenten en directe API-toegang. Pas
hostlijsten, procesnamen en domeinen aan op je eigen omgeving; de query's gebruiken placeholders.
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
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.
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.
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.
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
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.
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.
Kan ik Advanced Hunting draaien, en op welk account? Sommige tabellen lossen niet op vanaf een gewoon
account. Test met ai-probe-bronnen.kql.
Wat is de bron van de cloud-app-cijfers? Als die uit een andere integratie komen dan je
endpointtelemetrie, meet je twee verschillende populaties.
Bestaat endpoint-DLP? Zo niet, dan vervalt de hele datalaag en is dat zelf de bevinding.
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:
Fundament.ai-probe-bronnen en ai-noemer-apparaten: wat is haalbaar, en wat is mijn deler.
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).
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.
Losse bronnen.ai-browserextensies en ai-mailtoegang-apps.
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.
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
Systeemprocessen blazen je beeld op. De ingebedde assistent van het platform praat continu met zijn
eigen diensten. Filter die processen expliciet uit en schrijf op dat je dat deed.
Niet alles wat naar een AI-domein gaat is AI-gebruik. Trackingpixels in nieuwsbrieven lijken op een
AI-integratie in je mailclient. Toets op het startende proces voordat je duidt.
Vertrouw je eigen omgeving boven een zoekmachine. Producten met vergelijkbare namen bestaan volop; een
webzoekopdracht levert plausibele maar foute duidingen. Controleer de identiteit in de tenant.
De meetbron kan tussen twee rondes veranderen. Een inventaris die eerst leeg was en nu gevuld, is geen
toename in gebruik. Noteer de staat van de bron per ronde.
Verbindingen zijn geen gebruikers. Eén open tabblad telt zwaarder dan tien incidentele gebruikers.
Apparaataantal is de breedtemaat.
Aan is niet beheerd. De kernbevinding is meestal niet hoeveel er wordt gebruikt, maar dat niemand
eigenaar is van de laag waarin het gebeurt. Zie hoofdstuk 01, principe 3.
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
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