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>
31 lines
817 B
TOML
31 lines
817 B
TOML
[workspace]
|
|
resolver = "2"
|
|
members = ["crates/*"]
|
|
|
|
[workspace.package]
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
license = "MIT"
|
|
repository = "https://git.joelovestech.com/Hound/Antivirus.git"
|
|
|
|
[workspace.dependencies]
|
|
hound-supply = { path = "crates/hound-supply" }
|
|
anyhow = "1"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
clap = { version = "4", features = ["derive"] }
|
|
colored = "2"
|
|
time = { version = "0.3", features = ["serde", "std", "formatting"] }
|
|
inotify = "0.10"
|
|
yara-x = "1.19"
|
|
libc = "0.2"
|
|
sha2 = "0.10"
|
|
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
|
ureq = { version = "2", default-features = false, features = ["tls", "gzip"] }
|
|
hound-defs = { path = "crates/hound-defs" }
|
|
hound-mcp = { path = "crates/hound-mcp" }
|
|
|
|
[profile.release]
|
|
lto = true
|
|
strip = true
|
|
codegen-units = 1
|