Development¶
Set up¶
git clone https://github.com/tolecnal/pydebuprep && cd pydebuprep
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt && .venv/bin/pip install --no-deps -e .
| Command | Does |
|---|---|
make lint |
ruff (lint and format), flake8, mypy |
make test |
the test suite |
make coverage |
the tests with line and branch coverage, enforced at 100% |
make generated |
regenerate the example config and shell completions after changing commands, options or checks |
sudo make audit |
with the package installed: the read-only audit on this machine |
make deb |
build the package (needs debhelper dh-python python3-all python3-setuptools python3-yaml python3-pytest) |
The tests never touch the host: checks run against FakeSystem, an in-memory system
that refuses any command not on the allowlist.
The documentation¶
This site is built with MkDocs and
Material for MkDocs from docs/:
python3 -m venv .venv-docs
.venv-docs/bin/pip install -r requirements-docs.txt
.venv-docs/bin/mkdocs serve # http://127.0.0.1:8000, reloads on change
.venv-docs/bin/mkdocs build # strict: fails on broken links or missing pages
Facts that live in the code are generated, not written: each check page's
"At a glance" box and options, the checks index, the configuration reference and the
command-line reference come from tools/docs_hooks.py, which reads pydebuprep's
own registry, configuration schema and argument parser. tests/test_docs_site.py
fails if a check has no page, a page has no check, the navigation misses one, or
an allowlisted command isn't documented on any check page.
Every pull request builds the site; merges to main publish it.
Adding a check¶
- Create
src/pydebuprep/checks/<name>.pywith aChecksubclass: setid,title,category, andrequires_root/requires_commandsif needed, and implementrun(ctx) -> list[Finding]. Give every finding at least one remediation step. - To take part in snapshots, also implement
collect_baseline(ctx)andcompare(ctx, before). For settings, declareoptions = (Option("name", default, "help"),)and read them withself.opt("name"); the type comes from the default, and the option appears in the configuration reference,example-config,list-checks --verboseand config validation automatically. Overridevalidate()to check values against each other. - Add the class to
ALL_CHECKSinsrc/pydebuprep/checks/__init__.py. Itscategorymust be one ofCATEGORY_ORDERinsrc/pydebuprep/model.py. - Run commands only through
run_checked(ctx, argv), which turns a timeout or an unexpected exit status into a check error instead of empty output. For a new command, add its complete argv toALLOWED_COMMANDSinsrc/pydebuprep/system.py. Useapt_command()for anything apt. - Write tests under
tests/checks/withFakeSystemand real command output; coverage must stay at 100%. Map each option to the test that proves it intests/test_option_coverage.py. - Document it: a page
docs/checks/<id>.md(copy an existing one: purpose, what it checks, findings, snapshot, commands, limitations, and thefactsandoptionsmarkers), an entry inmkdocs.yml's navigation, a row in the README's checks table and an entry in the man page's CHECKS section. The docs tests fail until all of these match the code. Runmake generated. - Prove it's read-only: run the new command under
scripts/readonly-audit.shas root with the service it inspects installed, and add that package toscripts/ci-readonly-audit.sh(and the command toAUDIT_EXPECTthere) so every pull request is audited with it. Some obvious commands write (nginx -t,postfix check,ufw status); don't use those.
Releasing¶
Bump the version in debian/changelog (dch -v X.Y.Z) and in
src/pydebuprep/__init__.py together and merge that; then push a matching tag
from main (only maintainers can create release tags):
The Release workflow builds the package, audits it on every supported release, and publishes it with checksums and a provenance attestation only if everything passes.