After Dark Systems / Service Automation HubCompletely Programmable Infrastructure

OpenSAH / Farm tool guides

badfarm: read-only discovery.

Inventory and assess hosts, containers, Kubernetes and infrastructure code. Store the evidence, track findings over time, and hand reviewed work to the repair path.

Available Tag v0.4.0 Apache 2.0

What it is

badfarm inventories infrastructure and finds misconfiguration, unhealthy workloads, unsupported software, known vulnerabilities and the usual causes of an outage. Every assessment is saved to a local store, so each finding has a first-seen, last-seen and resolved time, and you can verify a fix against a checkpoint. Use it to answer "what is wrong across these hosts", to triage an outage, to troubleshoot one container or pod, or to scan infrastructure code before it is applied.

The same distribution ships goodfarm, which plans reviewed repairs from badfarm findings.

What it is not. badfarm never modifies its targets and never applies remediation. Every command it runs against a target must match a per-executable read-only grammar, enforced at runtime; anything else is refused before it starts and recorded as a coverage gap. Some allowed reads still leave side effects such as log lines or access times. Missing evidence is never reported as a clean result: a check that could not run is a coverage finding, not a pass.

Requirements

  • Python 3.9 or later. The core has no third-party dependencies.
  • Optional: PyYAML for YAML output and YAML infrastructure-code scanning (the yaml and iac extras).
  • For targets: ssh for ssh:// hosts, docker for container targets, and kubectl with a context for Kubernetes and k3s targets.
  • CI runs the suite on Linux and macOS.

Install

Install from a clone of the public repository. One install provides both the badfarm and goodfarm commands.

git clone https://github.com/afterdarksys/badfarm.git
cd badfarm
python3 -m pip install '.[yaml,iac]'

For development, use an editable install: python3 -m pip install -e '.[yaml,iac]'. Without installing, run python3 -m badfarm and python3 -m badfarm.goodfarm from the checkout.

Verify the install:

badfarm --version
badfarm doctor
goodfarm --help

badfarm doctor checks local prerequisites and the store location, never contacts a target, and exits 0 when ready.

Quick start

Start with the machine you are on, offline, so nothing leaves it:

badfarm --offline inventory local
badfarm scan local --checkpoint baseline
badfarm findings --severity critical,high
badfarm show bf-1a2b3c4d                    # any unique finding-ID prefix

Fix something, then confirm it against the checkpoint:

badfarm verify local --checkpoint baseline  # 0 only when baseline failures are resolved
badfarm drift local --checkpoint baseline   # 0 only when nothing changed

Then point it at a host you are authorised to assess:

badfarm scan ssh://web1.example.com
badfarm troubleshoot docker://local         # unhealthy containers, causes, read-only next steps

Key concepts

Targets

local
ssh://host
docker://local[/container]
docker+ssh://host[/container]
k8s://context[/namespace[/pod]]
k3s://context[/namespace[/pod]]

Evidence and findings

Each assessment records the evidence behind every finding (for example the kernel lines or package names that matched) and a receipt for each check that ran. A finding is marked resolved only when a complete assessment of the same target ran that check and did not observe it. An inventory, a run whose advisory source was unavailable, or a skipped check never resolves anything. show prints the evidence for one finding; explain says what evidence is missing and the next collection step.

Checkpoints

--checkpoint NAME labels an assessment. verify and drift re-scan and compare against it; checkpoints list shows what is stored.

Coverage

Unreachable hosts, denied reads, missing tools, stale datasets and skipped checks all become coverage findings. Exit codes treat them as needing attention, so an incomplete run cannot look clean.

Handoff and proposals

handoff FINDING --output handoff.json emits a simple vpscfgfarm handoff artifact. propose produces a versioned, expiring, review-only remediation proposal bound to the evidence, and validate-proposal checks its integrity against the store. Neither is an approval or permission to execute. Governed repair goes through goodfarm.

Usage

Global options (--db, --timeout, --budget, --offline, --log-level, --debug, -v and the data paths) go before or after the command. Every command accepts --format text and --format table.

Assess

badfarm scan ssh://web1.example.com --checkpoint before
badfarm scan local --profile posture        # profiles: default, posture, faults, exposure,
                                            # software, compromise, capacity, diagnostics
badfarm scan local --only configuration.resolver
badfarm scan local --skip exposure.unexpected-listener
badfarm checks                              # list every registered check
badfarm eol local                           # lifecycle findings
badfarm vulnerabilities local --stored      # vulnerability and patch findings, from the store
badfarm compromise local                    # confidence-scored compromise indicators

A fleet

Give any assessment a file of selectors. Output follows the file order, and one unreachable target never ends the run.

cat > targets.txt <<'EOF'
ssh://web1.example.com
ssh://edge1.example.com   # comments and blank lines are ignored
docker://local
EOF
badfarm scan --targets targets.txt --concurrency 8

For an outage across many hosts, triage is the fast path. It runs a short profile (failed units, kernel faults, disk and inode space, memory pressure, container and pod health) and writes nothing to the store unless you pass --save:

badfarm triage --inventory ssh-config                 # every Host alias in ~/.ssh/config
badfarm triage --inventory ansible:inventory/prod.ini --concurrency 256
badfarm triage ssh://web1.example.com ssh://edge1.example.com

For very large fleets, run badfarm on each host instead of over SSH: badfarm agent run writes a result file locally, the controller stores collected files with badfarm ingest, and badfarm fleet status reports on them.

Query what was already found

These read the store only. They never contact a target.

badfarm findings --severity critical,high
badfarm findings --older-than 30d --category vulnerability
badfarm findings --check-id health.kernel-fault
badfarm findings --only-resolved
badfarm history ssh://web1.example.com --since 2026-01-01T00:00:00Z
badfarm worklist                            # pending security updates and reboots, grouped
badfarm prioritize --ownership ownership.json

Troubleshoot containers and pods

badfarm troubleshoot TARGET diagnoses Docker and Kubernetes or k3s workloads: OOM kills, crash loops, image pull and configuration failures, unschedulable or not-ready pods, node pressure, services with no endpoints and warning events. Each diagnosis lists likely causes and the exact read-only commands to run next; --logs adds bounded, redacted log reads.

Scan infrastructure code

badfarm iac scan PATH statically reads Ansible, Terraform/HCL, Kubernetes manifests and compose files for changes that could cause an outage, lock an administrator out, open an exposure or leak a secret. Nothing in the tree is executed, rendered, planned or applied. Output is text, JSON or SARIF. Without the iac extra a tree with YAML reports incomplete, never pass.

badfarm iac scan .
python3 -m badfarm.iac scan ./infra --format sarif --fail-on medium > iac.sarif
python3 -m badfarm.iac scan ./repo --diff origin/main..HEAD    # pull-request gate: new findings only
python3 -m badfarm.iac rules

Investigations, accepted risk and backups

  • badfarm case tracks an investigation and preserves its evidence, including bounded log collection; cases can be verified, exported, retained, closed and pruned.
  • badfarm exception accept FINDING --target SELECTOR --owner alice --reason '...' --expires-at TIME records owned, scoped, expiring accepted risk (at most 90 days). It never resolves the finding or changes exit codes. exception list and exception revoke manage them.
  • badfarm backup --output backup.db writes a consistent private copy of the store.

Data, policy and the store

The store

The SQLite store defaults to $BADFARM_DB, otherwise ~/.local/state/badfarm/badfarm.db on Linux (honouring $XDG_STATE_HOME) and ~/Library/Application Support/badfarm/badfarm.db on macOS. --db overrides it. The directory is created 0700 and the database 0600; a state directory that is not a private directory you own is refused. Stores upgrade to schema 5 in place; back up with badfarm backup before upgrading.

Risk data

badfarm ships no exploitation, prediction or backport data, because a bundled copy goes stale silently. Supply each source yourself; an unconfigured, malformed or stale file becomes a coverage finding.

badfarm --kev-data ./data/kev.json \
  --epss-data ./data/epss.json \
  --backport-data ./data/debian-tracker.json scan local

Each data file carries schema_version, generated_at, max_age_days, its upstream source and an optional checksum. --offline disables active advisory queries; the missing coverage is reported, not hidden.

Policy data

Expected listener ports, expected setuid files, sshd policy, TLS certificate paths, DNS, TLS and HTTP checks, profile membership and similar settings are policy data, not code. Override them with --policy-data FILE:

{
  "schema_version": 1,
  "generated_at": "2026-08-28T00:00:00Z",
  "max_age_days": 3650,
  "source": "internal baseline",
  "policy": {
    "expected_listener_ports": [22, 443],
    "expected_setuid": ["/usr/bin/sudo"],
    "profiles": {"minimal": ["configuration.resolver"]}
  }
}

Exit codes

Exitscan, fleet runs and single-target assessments
0No findings needing attention and no reported incompleteness.
1A fail, warn or unknown finding, an unreachable target, or incomplete receipts.
2An invalid selector, or a handled request or controller error.
130Interrupted by the operator.

triage adds exit 3: no problems found, but some target was not fully triaged. worklist exits 1 when at least one host needs an upgrade or a reboot.

How it fits with the other tools

  • badfarm discovers. It is the evidence source for the rest of the chain.
  • goodfarm plans repairs from stored badfarm findings, sends approved requests to vpscfgfarm, and verifies recovery against a later badfarm checkpoint.
  • vpscfgfarm executes through its gates. Its deploy preflight and fleet iac command run badfarm's IaC scan when badfarm is installed.
  • oos is checked by badfarm's health.oos-agent probe on Linux hosts.
  • farmops catalog entries name badfarm check IDs in their prechecks and postchecks. farmops does not run them yet.

Built from the public badfarm repository (README.md, OPERATIONS.md, docs/TRIAGE.md, docs/IAC.md, docs/CONTAINERS.md and the badfarm --help output) at commit 99bad35.