Data & MoreEngineeringgithub.com/dataandmore/dlp

Stop the leak
at the source.

A multi-tenant Flask service that subscribes to Microsoft 365 change events, scans new email, Teams chat, OneDrive and SharePoint content for personal data, then blocks, quarantines or alerts on it before it spreads. The same audit trail that fires the block also gives the data owner a one-click revert.

Watch every change, classify in milliseconds, act before the page is refreshed.

4Microsoft 365 sources
5policy actions per source
2classifiers: regex + AI
60sdedup window per resource
Section 01Overview

What the service does

The DLP service sits between Microsoft 365 and the Data & More platform's classification stack. It subscribes to Graph change notifications for the tenants it serves, dedupes them, fetches the affected content, runs it through a fast regex prefilter and the platform's profilers, then maps the verdict to a per-tenant policy and acts on it inside the source system. Every action is written to an Elasticsearch audit index, and every block is paired with a revert link the data owner can use without leaving the email.

Subscribe

Listen to M365

Graph subscriptions are kept healthy by a background scheduler that renews tokens, reconciles drift, and re-applies bulk source configurations whenever the tenant's selection changes.

Classify

Regex first, AI second

Each item goes through a tenant-tuned regex prefilter (with ReDoS-protected timeouts). Only items that pass the prefilter are sent to the java_profiler for the heavier PII verdict.

Act

Block, quarantine or alert

The policy maps a (source, verdict) pair to one of five actions, all executed inside Microsoft 365: delete the message, move it to quarantine, redact a file, or just send a heads-up email or Teams card.

Section 02The pipeline

From webhook to enforcement

One change in Microsoft 365 fans out into one notification, one classification round trip, one policy decision and one audit row. The pipeline below is what every Teams message, OneDrive upload and SharePoint edit walks through.

INGRESS DEDUP FETCH CLASSIFY ACT SINK Microsoft Graph change webhook POST /webhooks/graph clientState validated return 202 immediately Dedup in-process map key = resource TTL 60s duplicates dropped Fetch content via Graph API teams: message body onedrive: file sharepoint: file exchange: quarantine Classify two-stage decision regex prefilter 1s timeout per pattern java_profiler circuit breaker, retry x3 Decide + act per-tenant policy allow alert block grace_block quarantine redact Audit + notify ES index email + Teams one row per item Graph lifecycle webhook reauth, missed, removed POST /webhooks/graph/lifecycle Background scheduler daemon threads, periodic profiler health, webhook health, drift reconcile, grace enforce Subscription drift re-apply on change hourly Grace enforce expired entries periodic sweep Elasticsearch shared substrate: dlp_config, subscriptions, transport_rules, pattern_library, audit, exclusions, fingerprints reads + writes
Main pipeline path Lifecycle / conditional branch Persisted / shared substrate
Section 03Sources

Four ways content can arrive

Each source has its own Graph subscription, its own resource path format, its own content fetcher, and its own notion of what blocking means. The processor routes on the parsed resource and dispatches into the right fetcher and the right enforcement.

SourceResource pathSubscribed viaEnforcement vector
Teams/teams/{id}/channels/.../messagesGraph change webhookReply with adaptive card, optional delete
OneDrive/users/{id}/drive/rootGraph change webhookBlock via Graph file actions, exclusion store
SharePoint/sites/{id}/drive/rootGraph change webhookBlock via Graph file actions, owners notified
Exchangetransport-rule quarantineEXO transport ruleScan-and-release flow, separate path
Subscriptions

How they stay healthy

  • The scheduler watches subscription expiry and renews via graph-management before tokens lapse.
  • The lifecycle webhook handles reauthorizationRequired (renew), missed (log; admin can re-apply) and subscriptionRemoved (log).
  • A drift reconcile job re-applies the bulk subscription config every hour for ALL-users and GROUPS modes; SELECTED-ids mode is left alone.
Exchange

Why it has its own lane

Exchange is wired through an EXO transport rule that quarantines suspicious mail before the user sees it. The DLP service then picks the message out of quarantine, classifies it, and either delivers or blocks it. Non-quarantine Exchange notifications are dropped on sight, they are legacy or misconfiguration.

Section 04Classification

Two classifiers, one verdict

Classification runs in two stages so the cheap one filters the bulk and the expensive one only sees what survives.

Stage 1

Regex prefilter

A per-tenant list of regex patterns, compiled on first use and re-compiled whenever the pattern list changes. Each match runs with a 1-second ReDoS-protected timeout (via the third-party regex module) so a single pathological pattern cannot stall the worker.

first hit wins. returns pattern name, or None.
Stage 2

java_profiler

If the prefilter does not short-circuit, the text goes to the java_profiler's /dlp/classify endpoint. The client wraps the call in a circuit breaker (5 failures, 60s reset) and a tenacity retry (3 attempts, exponential backoff on connection / timeout). On total failure it fails open as "no PII" rather than blocking innocent content.

request: text, name, type, companyId
response: pii: true | false
The AI profiler service is also wired in (for its NER and dictionary-driven categories) and shares the same health-monitor scaffolding, but the DLP fast path goes through the java_profiler's purpose-built /dlp/classify endpoint, which is optimized for binary block / allow decisions.
Section 05Actions

From verdict to action

The (source, verdict) pair is mapped to one of five actions by the per-tenant policy stored in dlp_config. Allow leaves the system silent; the other four all end with an audit row.

allow

No-op

Either no PII was detected, or the file is on the tenant's exclusion list for OneDrive / SharePoint. Nothing is written, nothing is sent.

alert

Heads-up only

Content stays in place. The sender (or the file owner) receives an email or Teams card noting that the content tripped the policy. Useful for low-severity categories.

block

Immediate enforcement

The enforcement layer takes action in the source: Teams message hidden behind a card, OneDrive / SharePoint file locked, Exchange mail dropped from quarantine. The audit row carries a revert token so the action is reversible.

grace_block

Human-verified

The file is left untouched and the owner receives an email with three options: enforce now, cancel (with optional temporary or permanent exclusion), or do nothing. After the grace window, the scheduler picks up the entry and enforces automatically.

quarantine

Exchange specific

Inbound mail caught by the transport rule sits in quarantine. The DLP service classifies it and chooses between release (deliver), keep (block) and notify (under review). A separate scan-and-release notification path keeps the sender informed.

revert

One-click undo

Every block-class action embeds a signed revert URL in the notification. The recipient clicks it and the revert_service reads reversal_data from the audit row to undo the action and write a revert audit row alongside.

Section 06Grace flow

The shape of a human-verified block

A grace_block is the soft option for OneDrive and SharePoint: nothing happens in the source until the owner either takes a decision or the grace window expires.

PII detected grace_block policy Audit row: pending grace_expires_at = +Nd Email to owner three signed links enforce now cancel do nothing enforce now dispatch block, status: enforced, revert link kept cancel, optional exclusion status: canceled, exclusion: none | temporary | permanent scheduler auto-enforces when grace_expires_at < now()
Block decision path Cancel branch Outcome / audit state
Section 07Notifications

Reach the owner where they already are

Notifications are addressed to whichever identity matters for the source (sender in Teams, file owner in OneDrive, site owners in SharePoint). The transport is picked per tenant.

Teams

Adaptive card, in thread

For Teams notifications the bot posts a proactive reply on the original thread (or a DM if the channel reply fails). The card surfaces the action, a justification snippet, a deep link to the message, and the revert URL.

Email

Graph mail or dm-notify

The email transport is chosen per tenant: graph-management sends as the tenant's own mailbox when Graph mail settings are configured, otherwise dm-notify sends from the platform. Templates are per-action with per-tenant overrides for heading / body / footer.

ActionSubject templateIncludes
blockData Protection Policy: Content BlockedRevert link
grace_blockData Protection Policy: Action RequiredGrace window + three signed links
alertData Protection Policy: Content AlertedHeading / body / footer only
quarantine, under reviewData Protection Policy: Email Under Reviewn/a
quarantine, deliveredData Protection Policy: Email Deliveredn/a
quarantine, blockedData Protection Policy: Email BlockedRevert link

Examples in the wild

The mockups below reproduce three real notifications the service sends: one Teams adaptive card, one block-class email, and one alert-class email. They are framed inside Field Manual exhibit cards but carry their own product colour identity so the reader sees what their users actually see.

Exhibit 01 | Teams adaptive cardsource: bot proactive reply
Data & More DLP08.39
DATA PROTECTION ALERT

Content Alert

Your content was flagged because it may contain sensitive data. Please review before sharing.


Content
(no subject)
Time
2026-06-03 06:39 UTC
Why was this flagged?

Analysis: This data contains personal data under GDPR regulations. The string "mit cpr er 010101…" likely represents a Danish CPR (Central Person Register) number, a unique identifier for an individual, thus falling under the definition of personal data.

This is an informational alert. No action has been taken on your content.

Exhibit 02 | Block emailsource: graph-management or dm-notify
GDPR ALERT

Content Blocked

Your content was Blocked by the organization's data protection policy. Check the dlp quarantine to find the original mail.

Action takenblock
Sourceexchange
SubjectDavid Junge shared "Red hat" with you
LocationView flagged content
Terms that triggered the policy:
  • red hat

If you believe this was a mistake, please contact your IT administrator.

Exhibit 03 | Alert emailsource: graph-management or dm-notify

Content Alert

Your content was flagged because it may contain sensitive data. Please review before sharing.

Action: Alert
Source: Sharepoint
Content: Document.docx
Time: 2026-06-02 23:01 UTC
Why was this flagged?

Analysis: This data contains personal data under GDPR regulations. It includes names ("Peter"), national identifiers ("NIN", "DNI", "número de identidad"), and identification numbers associated with individuals, along with details of family members' identification documents, clearly establishing the presence of personal information.

This is an informational alert. No action has been taken on your content.

This is an automated message from your organization's Data Loss Prevention system.
Section 08Background

What the scheduler is always doing

Four daemon threads start at app boot and never stop. They keep the service healthy without any cron or external orchestrator.

Job 01

Profiler health

Pings java_profiler and the AI profiler. Records up / down transitions and surfaces remediation actions.

Job 02

Webhook health

Every 15 minutes. Flags tenants whose Graph subscriptions have gone quiet for too long.

Job 03

Drift reconcile

Hourly. Re-applies bulk subscriptions for ALL and GROUPS modes so new users and group changes pick themselves up.

Job 04

Grace enforce

Walks the audit index for grace_block entries whose window has passed and dispatches the deferred enforcement.

The threads are daemons so the gunicorn worker can exit cleanly. State is held in Elasticsearch, not in memory, so any worker can pick up where another left off.
Section 09Under the hood

The stack

RuntimePython 3.12, uv, Flask + Gunicorn :8000
Web layer10 Flask blueprints (health, webhook, settings, transport-rules, pattern-library, audit, revert, subscriptions, testing, teams bot)
Resiliencepybreaker circuit breakers, tenacity retries, ReDoS-protected regex (regex module timeout)
Microsoft 365Graph change webhooks (Teams, OneDrive, SharePoint), EXO transport rules, graph-management proxy, Teams bot SDK
Classifiersjava_profiler for fast classify, AI profiler health-monitored alongside, in-process regex prefilter
Data storeElasticsearch via the dm-lib base model: dlp_config, subscriptions, transport_rules, pattern_library, audit, exclusions, fingerprints, azure_creds, azure_users, azure_sites, bot_chat_lookup, email_settings
Notificationsgraph-management /send-mail (tenant mailbox) or dm-notify /email/send (platform), Teams adaptive cards via the bot
BackgroundDaemon threads for health monitors, drift reconcile and grace enforce
DeploymentDocker, AWS ECR (eu-central-1), bash deploy.sh
In one lineThe shape of it

Subscribe, dedupe, classify, decide, act, audit.

From a Graph notification to an Exchange quarantine release, every Microsoft 365 change follows one tight loop, scoped to one tenant, in less time than the user takes to switch tabs. Every action is logged, every block is reversible, and every classifier is fail-open by design so the system never holds back content for the wrong reason.

github.com/dataandmore/dlp