-
Notifications
You must be signed in to change notification settings - Fork 0
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 |
For a normal CheckRegistry check, add matching file stems:
checks/<your_check>.pytests/fixtures/<your_check>.cyphertests/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 check has a matching test and fixture. Cross-domain checks follow the same rule under checks/cross_domain/ and tests/checks/cross_domain/.
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 findingsThe module-level pytestmark matters. Without it, the test lands in the fast lane and the Neo4j wipe guard will stop the run.
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" -vTear the container down when you are done:
docker rm -f adpf-test-neo4jFor the full local test workflow, use tests/README.md.
- 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.