This repository is detection-as-code for Microsoft Sentinel and Microsoft Defender XDR. Git is the source of truth; every rule change ships through a pull request that is reviewed, validated, and audited before deployment.
Before you start, please read:
CODE_OF_CONDUCT.md— community expectations.LICENSE— Apache-2.0 license terms.TRADEMARK.md— what you can and cannot do with theContentOpsandSecM8names.SECURITY.md— how to report a vulnerability privately.
git clone https://github.com/KustoKing/ContentOps.git
cd ContentOps
python -m venv .venv
# Activate (PowerShell):
.\.venv\Scripts\Activate.ps1
# Activate (bash / zsh):
# source .venv/bin/activate
python -m pip install -r requirements.txt
python -m pip install -e ".[dev]"
# Copy the tenant config template and fill in your Entra ID tenant + workspace IDs.
# config/tenant.yml is gitignored — it must never be committed. (See
# docs/operations/tenant-config-modes.md for the full set of supported
# tenant-config sources, including private-fork and vars+secrets-split
# alternatives.)
copy config\tenant.yml.example config\tenant.yml
# Install local pre-commit hooks (gitleaks, YAML checks).
# NOTE: gitleaks 8.21+ requires a free org license for organizational
# repos. If your repo is org-owned, skip `pre-commit install` for now,
# request the license at https://gitleaks.io/, and wire it as the
# `GITLEAKS_LICENSE` org secret when ready. CI's secret-scan.yml runs
# gitleaks regardless of local hooks.
python -m pip install pre-commit
pre-commit installAuthor-only contributors: if you only intend to author YAML detection content and let CI handle Azure operations, the
pip install -e ".[dev]"step above is the only setup you need. Skipaz login/.enventirely. Seedocs/quickstart.md§"Three adopter personas".
Azure authentication. Before you can run anything that touches the
tenant (contentops doctor --auth, contentops apply, the live test
suite), you need an Azure App Registration with the right permissions
plus credentials in your .env. If you've never set this up, walk
through
docs/operations/authentication-setup.md
— it explains what an App Registration is, what OIDC means, and the
portal steps in order. Already familiar with Azure auth? The TL;DR
section at the top of that doc has everything you need.
All commits must include a Signed-off-by: line. This is the
Developer Certificate of Origin
attestation that you have the right to contribute the change under
this project's license.
git commit --signoff -m "your message"
# or, set it for the repo:
git config commit.gpgsign true && git config format.signoff trueA missing sign-off will be flagged by the DCO check on the pull request and will block merge until fixed.
Branch protection on main MUST be configured manually in repository settings
— GitHub does not allow protected-branch rules to be modified through a PR.
See Managing a branch protection rule.
Recommended settings:
- Require a pull request before merging — at least 1 approval.
- Require review from Code Owners — enforces
.github/CODEOWNERS. - Require status checks to pass before merging, with the following checks
marked required (these are the GitHub Actions job/workflow names that exist
on
maintoday):pytest(job in.github/workflows/ci.yml)cli-smoke(job in.github/workflows/ci.yml)mitre-attack-coverage(workflow.github/workflows/coverage.yml)production-promotion-check(workflow.github/workflows/production-promotion-check.yml)
- Require branches to be up to date before merging.
- Restrict who can dismiss pull request reviews — admins only.
- Do not allow force pushes.
- Do not allow deletions.
CI runs a non-destructive cli-smoke job (W4.5-C) that exercises
the CLI's --help for the most-used subcommands. It catches
import-time regressions but does not replace local linting. Run
contentops lint and contentops doctor locally before opening a PR.
Every detection envelope carries a status field. The promotion lifecycle is:
development → testing → production → deprecated
Rules:
- Each transition is its own pull request. One rule per PR is preferred for production promotions so reviewers can focus on a single change.
- Production promotions trigger
production-promotion-check, which posts a sticky PR comment listing every rule whosestatuswas promoted toproduction(or added directly inproduction). - An emergency
contentops disableworkflow that bypasses normal review gates is deferred to a follow-up PR. The CLI commandcontentops disableexists today and can be invoked manually by an on-call operator.
Run these from the repository root before requesting review:
contentops doctor
contentops lint
python -m pytest tests/v2 -q
contentops coverage --path detectionscontentops is the only CLI entry point. python -m contentops is the
equivalent module-path invocation; both work after pip install -e ..
For credential-backed Azure validation, see
Live integration tests;
the page covers RUN_LIVE_TESTS=1, the INTEGRATION_* env vars, the
production-workspace guard, and PowerShell-vs-bash invocation
gotchas.
A PR that fails any of these locally will fail in CI. Fix issues before pushing to keep the review queue clean.
The expected baseline on main is all tests/v2 tests passing. New work
should preserve that contract.
If this is your first PR to a GitHub project, here's what happens after you push your branch. Each step is automatic unless noted.
-
Push the branch.
git checkout -b add-my-first-rule git add detections/sentinel_analytic/my-first-rule.yml git commit --signoff -m "Add: my-first-rule sentinel analytic" git push -u origin add-my-first-rule--signoffis the DCO attestation (see above). -
Open the pull request on github.com. Target branch:
main. Use the default PR template; describe what the rule detects and why. -
CI workflows fire automatically. You'll see status checks appear at the bottom of the PR within ~30 seconds:
dco— confirms every commit has aSigned-off-by:trailer.spdx-headers— confirms every Python file has the SPDX header.validate.yml— parses every envelope, runs handlervalidate().lint.yml— runscontentops lint(KQL + META rules).coverage.yml— posts an MITRE ATT&CK coverage delta as a comment. Never gates on its own.cli-smoke/pytest— unit tests + CLI sanity checks.gitleaks/bandit/semgrep— security scans.
A red check is blocking — branch protection won't let the PR merge until all required checks are green. Click any failed check to read the logs and fix the issue with another commit on the same branch; CI re-runs automatically.
-
Code review. A CODEOWNERS-listed reviewer leaves comments or approves. Resolve comments; reviewer re-approves; merge button unblocks.
-
Merge to
main. Use a squash merge for clean history (the default for this repo). Your branch is then safe to delete. -
deploy.ymlruns against the production tenant. This is the first time your rule actually touches Azure — until merge, everything was local/CI-only. The workflow:- Reads
config/tenant.ymlfrom theTENANT_CONFIG_YAMLsecret. - Authenticates via OIDC (no client secret).
- Runs
contentops apply --role prod --changed-since <prev-SHA>. - Writes one audit record per asset to
audit/<date>.jsonl. - Commits the updated audit log + state ref back to
main.
Watch the workflow in Actions tab. A failure here means the rule got through validation but ARM/Graph rejected it — see
docs/OPERATOR_GUIDE.md. - Reads
-
Drift PR tomorrow morning. The daily
drift.ymlworkflow collects live tenant state and compares to git. Your rule should appear asin-sync(or not appear at all — drift PRs only list differences). If it shows up aschanged, seeOPERATOR_GUIDE.mdRunbook 4.
That's the full loop. After your first PR, runs 2–4 are the daily cycle and you can ignore the rest.
If a check fails and you don't know why, read the logs first, then ask in the SOC team channel with: workflow name, failing job, log excerpt, and your branch name.
pyproject.toml [project.dependencies] is the canonical source of
truth for runtime dependencies. requirements.txt is a byte-mirror
maintained alongside it so pip install -r requirements.txt (used
by GitHub Actions and the setup-python@v5 pip cache) keeps working
without duplicating the dependency-resolution logic.
When updating a runtime dependency by hand:
- Edit the version pin in
pyproject.tomlfirst. - Update
requirements.txtto the same pin. - Commit both in the same change.
Renovate keeps the two in lockstep automatically via the config in
.github/renovate.json; the manual policy above only matters for
ad-hoc edits between Renovate runs. pip-audit in CI fails the
build if either file carries an unsuppressed advisory, so the worst
case of drift is a noisy CI rather than a silent supply-chain risk.
The repository keeps the collect / export capability (the
collect.yml workflow exports live tenant
state into detections/). Generated export output is the result of that
runtime workflow — do not commit generated detections/** changes as part
of an unrelated PR. Promote rule changes intentionally, one PR at a time.
All commits merged to main should be cryptographically signed and show
GitHub's Verified badge.
To enforce this, enable "Require signed commits" under
Settings → Branches → Branch protection rules → main. It cannot be
configured through a pull request — a repository administrator must
enable it.
# 1. Tell git to sign with SSH using your existing key.
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
# 2. Upload the same public key to GitHub as a "Signing key"
# (Settings → SSH and GPG keys → New SSH key → Key type: Signing Key).# 1. Generate (or import) a GPG key, then tell git about it.
gpg --full-generate-key # ed25519 recommended
KEYID=$(gpg --list-secret-keys --keyid-format=long | awk '/^sec/{split($2,a,"/"); print a[2]; exit}')
git config --global user.signingkey "$KEYID"
git config --global commit.gpgsign true
# 2. Export the public key and add it under
# Settings → SSH and GPG keys → New GPG key.
gpg --armor --export "$KEYID"After setup, verify with git log --show-signature -1 and confirm the
Verified badge appears next to your commit on GitHub.