Closes Phase 3's delivery half. `hound update` now asks the definitions
host what exists, downloads what this machine lacks, verifies it, and
installs it. Verified end to end against the live host over TLS:
starting from an empty directory:
defs: no verified packs were found
hound update:
definitions: 1 pack(s) installed, 0 already current
crates-io-2026.08.21.pack — 19 indicators, version 2026.08.21
loaded 19 indicators from 1 pack(s) [2026.08.21]
and immediately afterwards a lockfile naming rustdecimal is flagged,
while tokio beside it is not.
running it again:
definitions: definitions are up to date (1 pack(s))
a pack with one byte flipped in its signed payload, advertised in the
index with a correct hash and a version of 9999.1.1:
definitions: 0 pack(s) installed, 1 up to date, 1 REJECTED
tampered-test.pack: the definitions pack is not signed by Hound and
was discarded
Three refusals are the design:
* THE INDEX IS A HINT, NEVER AN AUTHORITY. It says which packs exist and
what they hash to, and both are unverified — anyone who can serve the
index can lie about either. Only the Ed25519 signature decides whether
a pack is real. A tampered index can waste bandwidth and nothing else,
which is exactly what the test above demonstrates: correct hash,
higher version, still refused.
* NOTHING UNVERIFIED REACHES THE DEFINITIONS DIRECTORY. Downloaded to a
temp file, verified there, then moved with a rename inside the same
directory so it is atomic. The daemon cannot observe a half-written
pack, and a crash mid-download leaves a stray temp file rather than a
loadable one.
* A FILENAME FROM A REMOTE INDEX IS UNTRUSTED INPUT. The updater runs as
root, so an entry of ../../../etc/cron.d/evil.pack would be remote code
execution. Only a plain basename ending in .pack, with no separators,
no dot-dot and no leading dot, is accepted. Tested against eight
hostile shapes.
Sizes and timeouts are bounded — a pack is a list of package names, so a
server offering a hundred gigabytes is broken or hostile and the
difference does not matter to a full disk. The read is bounded
independently of Content-Length, which is also just something the server
said.
Definitions are fetched BEFORE rules are reloaded, and a failure to fetch
does not stop the reload. No network, a mirror down, a pack that will not
verify — none of those are a reason to skip the half that still works.
HOUNDD_DEFS_URL points the client at a mirror, which matters for the
air-gapped deployments that are a real part of the Fleet story.
build-pack now writes index.json beside the pack, so publishing is one
command.
358 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
26 lines
618 B
TOML
26 lines
618 B
TOML
[package]
|
|
name = "houndd"
|
|
description = "Hound Antivirus daemon — Unix-socket JSON-RPC over a pluggable engine"
|
|
version.workspace = true
|
|
edition.workspace = true
|
|
license.workspace = true
|
|
repository.workspace = true
|
|
|
|
[[bin]]
|
|
name = "houndd"
|
|
path = "src/main.rs"
|
|
|
|
[dependencies]
|
|
hound-api = { path = "../hound-api" }
|
|
hound-supply.workspace = true
|
|
anyhow.workspace = true
|
|
serde.workspace = true
|
|
serde_json.workspace = true
|
|
time.workspace = true
|
|
inotify.workspace = true
|
|
yara-x.workspace = true
|
|
libc.workspace = true
|
|
sha2.workspace = true
|
|
hound-defs.workspace = true
|
|
ed25519-dalek.workspace = true
|
|
ureq.workspace = true
|