Skip to main content

host-triage

CI Python Ruff License: MIT

One command to answer "is this host actually up, and if not, where does it break?"

host-triage runs the checks you'd otherwise run by hand - DNS resolution, ping, a TCP connect, TLS certificate expiry, and an HTTP request - against one or more targets (concurrently), then prints a compact table (or JSON) and exits with a code you can gate on. Opt-in checks cover traceroute, SPF, DMARC, and HTTP security headers.

$ host-triage api.example.com
host-triage  api.example.com  (1 check(s) failed)

  OK    dns   resolved 2 addresses (93.184.216.34, 2606:2800:220:1::1)   11 ms
  OK    ping  reachable, avg 9.5 ms                                      42 ms
  OK    tcp   connected to 93.184.216.34:443                             41 ms
  WARN  tls   certificate valid, expires in 9 day(s) (2026-08-31)        44 ms
  FAIL  http  503 Service Unavailable                                   120 ms

$ echo $?
1

Why

When something is unreachable, the first five minutes are always the same sequence of dig, ping, nc -zv, curl -I, and openssl s_client to find the layer that's broken. This collapses that into one call with consistent output.

  • Zero runtime dependencies. Pure standard library; runs anywhere Python 3.11+ does.
  • Scriptable. --json for machines, exit codes for pipelines.
  • Layered. Each check is independent, so you see exactly which layer fails.

Install

# with pipx (recommended for a CLI)
pipx install git+https://github.com/pearlab0x/host-triage

# or from a clone
git clone https://github.com/pearlab0x/host-triage
cd host-triage
pip install .

No install needed to try it - python -m host_triage example.com works from a clone.

Usage

host-triage example.com                     # default checks: dns, ping, tcp, tls, http
host-triage https://api.example.com:8443    # scheme and port taken from the URL
host-triage db.internal -c dns,tcp -p 5432  # only some checks, explicit port
host-triage example.com --trace             # add traceroute (slow, off by default)
host-triage example.com -a                  # every check
host-triage example.com --json | jq .       # machine-readable (needs jq)
host-triage example.com -t 2                 # 2-second per-check timeout
host-triage example.com --spf                # also resolve the SPF record
host-triage example.com --dmarc              # also resolve the DMARC record
host-triage example.com --headers            # also audit HTTP security headers
host-triage example.com --fail-on warn       # exit non-zero on warnings too
host-triage example.com --watch 10           # re-check every 10s until ctrl-c
host-triage example.com -c spf --json | jq .details   # just the SPF, structured (needs jq)

Failing on warnings

By default only a FAIL breaks the run, so a certificate with 9 days left exits 0. --fail-on warn moves the gate down a level, which is what you usually want in CI:

$ host-triage example.com --fail-on warn
host-triage  example.com  (1 check(s) at or above warn)

  WARN  tls  certificate valid, expires in 9 day(s) (2026-09-14)  44 ms

$ echo $?
1

Skipped checks never trip the gate, and the count in the header always matches the exit code.

Watching a host

--watch re-runs the same checks until you stop it - useful while waiting for a deploy to come back or a DNS change to propagate:

host-triage api.example.com --watch        # every 5s, redrawn in place
host-triage api.example.com --watch 30     # every 30s
host-triage api.example.com -w 10 --json   # one JSON document per cycle

On a terminal it clears and redraws; piped or with --json it just appends, so the output stays usable downstream. The exit code is that of the last pass.

Multiple targets

Pass several targets at once and they're checked in parallel:

host-triage a.example.com b.example.com api.example.com:8443
host-triage -f hosts.txt                      # one target per line ('#' comments ok)
cat hosts.txt | host-triage                    # or piped on stdin
host-triage -f prod.txt -f staging.txt -j 20   # multiple files, 20 workers

Results are always printed in the order the targets were given, regardless of which finishes first, and the run exits non-zero if any target has a failure. A single unparseable target reports a failing target check instead of aborting the whole batch. Worker count is chosen automatically; override it with -j/--jobs.

Targets

Accepts host, host:port, or a full URL, including bracketed IPv6:

example.com            example.com:8443       https://example.com/health
[2606:4700:4700::1111] 1.1.1.1:853            http://api.internal:8080

Checks

Check What it verifies Notes
dns Hostname resolves to one or more A/AAAA records
ping ICMP reachability and average RTT Shells out to the system ping
tcp A TCP connection to the port can be established Reports the peer IP and connect latency
tls Certificate is valid and not expiring soon WARN within --tls-warn-days (21)
http An HTTP(S) request returns a non-5xx status Follows redirects; 4xx warns, 5xx fails
headers Response carries the standard security headers Opt-in via --headers
trace Traceroute hop count to the host Opt-in via --trace; can be slow
spf Looks up the domain's SPF (TXT) record and its origins Opt-in via --spf; needs dig on PATH
dmarc Looks up _dmarc.<domain> and reads the policy Opt-in via --dmarc; needs dig on PATH

Statuses are OK, WARN, FAIL, and -- (skipped, e.g. tls on a plain http:// target).

Security headers

headers looks for HSTS, CSP, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy, and warns on anything missing. It also flags headers that are present but do not deliver what they promise - an HSTS max-age under six months, or an X-Content-Type-Options that isn't nosniff. HSTS is not counted over plain http://, where it means nothing, and an error response (say a 403) is still audited rather than discarded.

$ host-triage example.com -c headers
  WARN  headers  3 of 5 security header(s) present (missing: CSP, HSTS)

DMARC

dmarc resolves _dmarc.<domain> and parses the policy tags. Because the point of the check is whether the domain is actually protected, a record that enforces nothing is a warning: no record at all, more than one, no p= tag, p=none, or a pct below 100.

$ host-triage example.com -c dmarc
  OK    dmarc  DMARC: policy p=reject, sp=reject, 1 rua

JSON output

$ host-triage example.com -c dns,tls --json
{
  "target": "example.com",
  "ok": true,
  "worst": "ok",
  "checks": [
    {
      "name": "dns",
      "status": "ok",
      "summary": "resolved 1 address (93.184.216.34)",
      "details": { "addresses": ["93.184.216.34"] },
      "duration_ms": 11.4
    },
    {
      "name": "tls",
      "status": "ok",
      "summary": "certificate valid, expires in 61 day(s) (2026-10-22)",
      "details": { "days_left": 61, "not_after": "2026-10-22", "issuer": "Let's Encrypt" },
      "duration_ms": 44.1
    }
  ]
}

For a single target the JSON is the flat object shown above. For multiple targets it's wrapped so you can gate on the batch as a whole:

{
  "ok": false,
  "failures": 1,
  "targets": [ { "target": "a.example.com", "ok": true,  "checks": [ ... ] },
               { "target": "b.example.com", "ok": false, "checks": [ ... ] } ]
}

Exit codes

Code Meaning
0 Nothing reached the threshold on any target
1 One or more checks reached it on one or more targets
2 Usage error
3 Unexpected error

The threshold is fail by default, so a warning (e.g. a cert with 9 days left) does not break the run - wire it into CI and only fail on a real outage:

- name: Smoke-test production
  run: host-triage https://example.com/health --json

Or tighten it so an expiring certificate breaks the build before it expires:

- name: Certificate and header posture
  run: host-triage https://example.com --headers --fail-on warn

Development

pip install -e ".[dev]"
ruff check . && ruff format --check .
mypy
pytest --cov=host_triage

The suite mocks all network I/O, so it's fast and runs offline. CI checks lint, formatting, and types, runs the tests on Linux, macOS, and Windows across Python 3.11–3.13 against an 80% coverage floor, then builds the sdist and wheel and smoke-tests the installed package. Actions are pinned to commit SHAs and kept current by Dependabot.

Releasing

Releases are tag-driven. Bump the version in pyproject.toml and src/host_triage/__init__.py, add the CHANGELOG.md section, then:

git tag v0.4.0 && git push origin v0.4.0

release.yml verifies the tag matches the packaged version, publishes to PyPI via Trusted Publishing (no token stored in the repo), and cuts a GitHub Release with notes from the changelog. The PyPI project must have this repo, release.yml, and the pypi environment registered as a trusted publisher before the first run.

Design notes

  • No third-party runtime deps keeps install trivial and the supply chain small.
  • Classification lives in pure helpers (classify_http_status, classify_cert_days, parse_ping_rtt, …) so the interesting logic is unit-tested without sockets.
  • ping/traceroute shell out to the system binaries rather than opening raw ICMP sockets, which would require elevated privileges.

License

MIT - see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

host_triage-0.4.0.tar.gz (31.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

host_triage-0.4.0-py3-none-any.whl (23.1 kB view details)

Uploaded Python 3

File details

Details for the file host_triage-0.4.0.tar.gz.

File metadata

  • Download URL: host_triage-0.4.0.tar.gz
  • Upload date:
  • Size: 31.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for host_triage-0.4.0.tar.gz
Algorithm Hash digest
SHA256 67fdf11f43d954de3b07a62cb0f513ec0bba41703819fe30090e9cceba06a691
MD5 42e21431d7800e5579c6d94c75d5143c
BLAKE2b-256 dfa9a5db8b2d3103c9bf85d4048bac2a7e26ef8846f9ece121b9f308e084d4fc

See more details on using hashes here.

Provenance

The following attestation bundles were made for host_triage-0.4.0.tar.gz:

Publisher: release.yml on pearlab0x/host-triage

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file host_triage-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: host_triage-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 23.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for host_triage-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1874e7662fae3f8579d9a547da1e91a89c5b350b9dd31a02e2abde9111efaf9e
MD5 5ad9a27b66196894561237ca113feaaf
BLAKE2b-256 9b6f986985bfdb330f90bd179d3b29b608ab9c97d5a435e4ea342bee2d4c984b

See more details on using hashes here.

Provenance

The following attestation bundles were made for host_triage-0.4.0-py3-none-any.whl:

Publisher: release.yml on pearlab0x/host-triage

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page