Manage Microsoft Purview DLP policies as version-controlled code. Write policies in a small YAML language, compile and validate them offline, and deploy them through a reviewable, simulation-first pipeline.
name: block-external-pci-exchange
archetype: group-scoped-simulation
description: Block credit-card data emailed outside the org.
locations:
exchange: true
rules:
- name: block-external-pci-exchange-Financial
detect:
accessScope: NotInOrganization
groups:
- name: Financial
sensitiveTypes:
- {name: Credit Card Number, confidence: High}
actions:
blockAccess: true
generateIncidentReport: [admin@example.com]That compiles to a deploy manifest with every name resolved to the GUID Purview requires. A name that is not in the catalog fails the build rather than deploying a policy that silently matches nothing.
Why this exists. DLP is normally managed by clicking through a portal, where a misconfigured rule does not fail loudly, it just matches nothing, and zero alerts looks exactly like zero violations. Catching that requires validation, review, and staged rollout before deployment. See docs/rationale.md for the full argument.
To author, compile, and test, which is everything except talking to a tenant:
- Python 3.10 or later (
jsonschema==4.26.0does not support earlier versions; CI uses 3.12) - No tenant, credentials, or network access
To deploy to Purview, additionally:
- Windows with PowerShell 5.1, since
Connect-IPPSSessionis Windows-only - The
ExchangeOnlineManagementmodule (the scripts install it if missing) - An Entra app registration with certificate auth and Purview permissions, plus an Azure Key Vault holding the certificate
git clone https://github.com/s0undsystem/DLP-as-Code.git
cd DLP-as-Code
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt # editable install of the project and its depsThat installs dlpac as a package, so compiler and assistant import from anywhere and a
dlpac-compile command is available. For the Claude backend, pip install -e '.[claude]'.
python -m compiler.compile # policies/*.yaml -> build/manifest.json
python tests/test_compiler.py # 13 tests
python tests/test_assistant.py # 12 testsdlpac-compile and python compiler/compile.py do the same thing. It prints the number of
policies compiled and writes build/manifest.json. Nothing here
touches a tenant, so this works on macOS, Linux, and Windows alike.
Add a YAML file to policies/, then recompile. The example above is a complete policy.
locationsselects surfaces:exchange,teams,sharePoint,oneDrive,endpoint,copilot.scope.groupfences a policy to a group; Copilot usesscope.usersinstead.archetypeinherits shared defaults fromarchetypes/, such as simulation mode plus pilot-group scope. Policy keys override archetype keys.- Sensitive information types, sensitivity labels, groups, and sites are written as human names and
resolved from
catalog/catalog.jsonat build time.
Existing examples in policies/ cover label-based detection, financial identifiers, a
Copilot-scoped policy, and a rawAdvancedRule passthrough for detection the structured form does
not yet express. Full field reference: docs/dsl-reference.md.
Set these as repository variables:
AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, KEY_VAULT_NAME, CERT_NAME,
M365_ORGANIZATION
Four workflows implement the pipeline:
| Workflow | Trigger | Runner | Effect |
|---|---|---|---|
validate |
push, pull request | ubuntu-latest |
compile and test; no tenant access |
plan |
pull request | windows-latest |
read-only diff of manifest against the tenant |
deploy |
manual dispatch | windows-latest |
applies the manifest |
drift |
daily cron | windows-latest |
exports the tenant, opens a PR on divergence |
Enforcement is gated. deploy takes an allow_enforce input which sets DLP_ALLOW_ENFORCE, and
Invoke-Deploy.ps1 refuses any policy with Mode: Enable unless it is true, halting the run.
Simulation modes deploy freely. Point deploy at a protected environment with required reviewers to
make enforcement a human decision as well as a flagged one.
A run reports N deployed, N skipped, N failed and exits non-zero if any failed. Skipped means the
policy was processed but left incomplete, usually a rule still in PendingDeletion; re-run once the
service finishes.
Refresh the catalog from your own tenant with powershell/Update-Catalog.ps1. The sensitive
information type GUIDs shipped in catalog/catalog.json are Microsoft's global built-in
identifiers and are the same in every tenant; groups, sensitivity labels, sites, and custom types
are placeholders that must be replaced.
To run locally rather than in CI, dot-source powershell/Connect-Dlp.ps1 first so it exports
DLP_CERT_THUMBPRINT into the same process, then run Invoke-Plan.ps1 or Invoke-Deploy.ps1.
An optional layer drafts policies from plain English and answers coverage questions. Every draft is pushed through the same compiler a hand-written policy goes through, and one that does not compile cannot open a pull request.
# No model at all: exercises the full path with no key, network, or cost
DLPAC_BRAIN=dry-run python -m assistant.eval
# Local OpenAI-compatible endpoint (Ollama, LM Studio, vLLM)
export DLPAC_BRAIN=local
export DLPAC_LOCAL_BASE_URL=http://localhost:11434/v1
export DLPAC_LOCAL_MODEL=qwen2.5-coder:14b
# Anthropic API
export DLPAC_BRAIN=claude ANTHROPIC_API_KEY=...
export DLPAC_CLAUDE_MODEL=claude-opus-5 # optional; overrides the default model
pip install anthropic # optional dependency, only for this backend
python -m assistant.eval --show-draftseval reports how many drafts compile. That measures form, not intent: a policy can compile
perfectly and still watch the wrong data type, which is why output is always a pull request for a
human to review. Details in
docs/architecture.md.
pyproject.toml packaging: dependencies, entry points, project metadata
compiler/ schema, validator, name-to-GUID resolver, compiler
catalog/ sample catalog (global built-in SIT GUIDs; placeholders elsewhere)
archetypes/ reusable policy defaults
policies/ example policies
powershell/ Connect, Plan, Deploy, Export, Update-Catalog, Remove (Windows only)
assistant/ natural-language layer: brain interface, backends, validator, eval
tests/ unit tests, no tenant access
docs/ rationale, architecture, DSL reference
.github/ validate, plan, deploy, drift workflows
| Document | Contents |
|---|---|
| docs/rationale.md | Why DLP belongs in code, related work, design principles, portability |
| docs/architecture.md | Pipeline contracts, Purview service behaviours, the assistant, safety model, open problems |
| docs/dsl-reference.md | Every field in the policy language |
| CONTRIBUTING.md | Setup, house rules, what to work on |
| SECURITY.md | Reporting vulnerabilities, scope, operational notes for forks |
If you are deciding whether to adopt this, read the rationale. If you are extending it, read the architecture, particularly the substrate findings: several non-obvious Purview behaviours constrain what the deploy engine is allowed to do.
Issues and pull requests are welcome. The compiler, the assistant, and both test suites run with no tenant access, so most contributions can be developed and verified entirely offline. See CONTRIBUTING.md for setup and house rules, and open problems for the known unsolved work.
Reports of how the Purview service actually behaves are especially welcome. Much of the deploy engine's shape comes from undocumented service behaviour discovered against a live tenant, and that knowledge is expensive to rediscover.
If you build on the methodology or the code, see CITATION.cff. GitHub renders it as a "Cite this repository" option in the sidebar.
Apache License 2.0, Copyright 2026 Jared Medeiros. See NOTICE.
Apache 2.0 permits commercial use, modification, and redistribution, including in closed-source products. It requires that you preserve the copyright and NOTICE attribution, state any changes you made, and include a copy of the license. It also grants an express patent license from contributors, and terminates that grant for anyone who initiates patent litigation over the software.
