Skip to content

Adding New Checks

danti1988 edited this page May 18, 2026 · 5 revisions

Adding New Checks

Checks are small classes under checks/. They register with @check(...), declare the data they need, and return finding dictionaries keyed by SID or by a stable finding name. checks/EXAMPLE_TEMPLATES.py is not imported; it is there only as a set of starting points.

Start by choosing the pattern that matches the finding.

If your check... Use this pattern Reference
Reads preloaded user or computer properties AD check Writing an AD check
Runs Cypher against MSSQL or SCCM nodes Platform check Writing a platform check
Detects an ADCS template or CA issue ADCS check Writing an ADCS check
Uses nodes or edges from a new OpenGraph collector OpenGraph check OpenGraph plugins
Compares data across domains Cross-domain check Writing a cross-domain check

Files to Add

Most new checks are normal CheckRegistry checks. Add matching file stems:

  1. checks/<your_check>.py
  2. tests/fixtures/<your_check>.cypher
  3. tests/checks/test_<your_check>.py

Files under checks/ are imported automatically, so you do not edit a central registry. The inventory test enforces that every registered standard check has a matching test and fixture.

For cross-domain checks, add checks/cross_domain/<your_check>.py and tests/checks/cross_domain/test_<your_check>.py. Add a fixture only when the check needs Neo4j graph data; NTDS-only cross-domain checks can use mock data in the test.

If a check depends on optional OpenGraph data, add or reuse a datasource gate in datasources/<platform>.py and declare it with requires=["<platform>"]. This keeps normal AD audits quiet when the collector data is not present.

The fixture test is not optional for a new check. Keep the graph as small as possible, load it with load_fixture(...), run the check with run_check(...), and assert the exact finding behaviour.

import pytest

from checks.your_check import YourCheck
from tests.check_harness import load_fixture, run_check

pytestmark = pytest.mark.neo4j


def test_fires_on_minimal_graph(clean_neo4j):
    load_fixture(clean_neo4j, "your_check.cypher")
    findings = run_check(YourCheck, clean_neo4j)
    assert findings

The module-level pytestmark matters. Without it, the test lands in the fast lane and the Neo4j wipe guard will stop the run.

Before Opening a PR

Install the test extras once:

pip install -e ".[test]"

Run the fast framework suite. This is the same selector shown in the README:

pytest tests/ -m "not neo4j and not integration"

Then run the Neo4j fixture suite against a disposable database. The fixture suite wipes the target database before and after each test, so do not point this at a real BloodHound instance.

docker run -d --name adpf-test-neo4j \
  -p 17474:7474 -p 17687:7687 \
  -e NEO4J_AUTH=neo4j/testpassword \
  neo4j:5-community

until docker exec adpf-test-neo4j cypher-shell -u neo4j -p testpassword "RETURN 1" >/dev/null 2>&1; do sleep 2; done

ADPF_TEST_ALLOW_WIPE=1 \
NEO4J_URI=bolt://localhost:17687 \
NEO4J_USER=neo4j NEO4J_PASSWORD=testpassword \
pytest tests/checks -m "neo4j and not integration" -v

Tear the container down when you are done:

docker rm -f adpf-test-neo4j

For the full local test workflow, use tests/README.md.

PR Checklist

  • The check registers with @check(...) or the cross-domain registry.
  • data=[...] contains only data the check actually reads.
  • Platform checks use requires=[...] so they are skipped when the datasource is absent.
  • Domain filtering is applied where a query could otherwise cross domain boundaries.
  • A minimal positive fixture test exists, and a negative fixture test exists where the false-positive risk is non-trivial.
  • The fast suite and the Neo4j fixture suite both pass.

Clone this wiki locally