Commit graph

3 commits

Author SHA1 Message Date
dev
38d1feeb0c 0.1.1: signed release notifications, and definitions that actually update
Two mechanisms Joe asked for, neither of which existed.

**Definitions now update themselves.** `auto_update_signatures` shipped
from the start and nothing ever read it — definitions only moved when
somebody typed `hound update`, which for a security product means most
installations were running whatever they were installed with. There is
now a scheduler thread: definitions hourly, release check daily, and a
90-second startup delay so a fleet that reboots together does not
arrive at the CDN in one wave. It calls the same install path as
`hound update` rather than a second implementation that could drift.

**Release notifications, signed.** dl.houndav.com/latest.json carries
an Ed25519 signature from the same key as the definition packs, for the
same reason: whoever serves that host must not be able to invent a
version and point our users at a binary of their choosing. A manifest
that does not verify is discarded, and no manifest means "nothing newer
known" — never "update available". The safe default is silence.

The signature covers a canonical byte form defined identically in
release.rs and tools/publish-release.py. Signing a re-serialisation of
a parsed struct is the classic way to verify one thing and act on
another, so a test runs a real manifest produced by the Python tool
through the Rust verifier against the production key. If those drift,
manifests verify nowhere and the only symptom is that nobody ever hears
about an update.

Version comparison is numeric, not lexical, because "0.9.0" > "0.10.0"
as strings and that is precisely the pair where it would first be
noticed. Downgrades are never offered — a signed-but-old manifest must
not become a way to reintroduce a fixed vulnerability.

The app does not replace its own binary. A root daemon that can rewrite
itself is the mechanism a supply-chain attacker most wants. Installing
goes through apt with the user present and authenticating: the package
is downloaded, checked against the manifest's SHA-256, staged into a
root-owned 0700 directory so nothing can substitute it between the
check and the read, and handed over. Two guards beyond the signature —
the URL must be on our own download host, because a signature proves
publisher intent and not that the publisher got the URL right.

Tray, per Joe's design: a new amber "attention" state ranked below a
threat and an in-flight scan, above protected. Deeper amber than
"scanning" so the two are distinguishable — scanning lasts seconds and
the user started it, attention persists. The tooltip carries the reason
rather than only that there is one, and "Install Hound X…" appears
enabled only when there is something to install; a permanently greyed
item teaches people the menu is decorative.

Definition staleness is a first-class signal, not just update
availability: amber at 7 days, and at 30 the wording stops pretending —
"this machine is not currently protected against anything found since
then". An antivirus showing green on month-old definitions is lying in
the same way a dead front-end showing "Protected" was.

Also: the four files that declare a version now have a test asserting
they agree. Drift there means a release looks older than what is
installed and the update silently never offers, or offers forever.

377 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:58:15 -05:00
Hound
2cf9a740c3 rules: every rule needs an anchor, and the test now proves it for all of them
Stage 3 passed on throughput — 27,339 events, zero rescues, Caddy
unmoved — and then the soak found what the load test could not.

Two more false positives, both the same shape as the ones before:

* EICAR matched an 8.5 MB rustc incremental-compilation cache, because
  the test source being compiled contains the literal. Hound moved it to
  quarantine mid-build and rustc panicked. The standard defines the
  EICAR file as exactly that 68-byte string, optionally padded to 128,
  so the rule now says filesize <= 128.

* The webshell rule matched a 4.3 MB AI session transcript, because the
  conversation had been discussing webshells and therefore contained
  "<?php", the eval pattern and "$_POST". The transcript was moved to
  the vault and its history lost. A webshell is a PHP file: small, and
  opening with a PHP tag. Now filesize < 1MB and $php in (0..4096).

The interesting part is why the second one happened at all. After the
first, I added a test asserting that a large file containing rule
strings is not a threat — and hand-listed the strings. I listed the
miner's and the rootkit's and forgot "<?php". The test passed and the
transcript was quarantined anyway.

So the test now extracts every string literal from the rule pack itself
and builds the haystack from those. A rule added tomorrow is covered
without anybody remembering to cover it. It also asserts the extractor
actually found the strings, because a parser that silently returns
nothing would make the whole thing vacuous.

Both fixes have a paired test that the detection still works: a real
68-byte EICAR file is caught, padded to 128 it is caught, and a real
webshell is caught.

Worth recording, because it is not a bug: six houndd tests failed while
the gate was armed. Hound quarantined the EICAR fixtures the test suite
had just written — correct behaviour, colliding with a suite that
creates real malware samples. Running the antivirus's own tests on a
gated machine needs thought; the tests are not wrong and neither is the
gate.

The definitions chain now works end to end: pack built from OSV, signed
with the release key, published to /srv/houndav/defs, installed, and
verified on load against the public half compiled into the agent —
"defs: 19 indicators from 1 pack(s) [2026.08.21]".

The public key is in the source on purpose. The agent is open source and
anybody should be able to check that the definitions they received are
the ones we published.

300 tests pass. Gate is off pending these fixes being soaked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:37:49 -05:00
Hound
42cd97d59f supply: read lockfiles, and only ever call a malicious package malicious
Lockfile parsing for npm (all three lockfile versions), yarn, cargo,
poetry, requirements.txt, go.sum, Gemfile.lock and composer.lock, wired
through the indicator index so a sweep checks real dependencies against
real definitions. Signed packs load in the daemon; the sweep gets the
index; `hound supply-chain` cites the OSV record it matched.

A lockfile is the right thing to read: it names every transitive
dependency at an exact version in one small file, and it lists what WILL
be installed rather than what already is — which matters when the
payload runs during installation.

Every parser is hand-written rather than pulling in a TOML and a YAML
crate. Two fields from each format, and a scanner parsing hostile input
should have as little parsing surface as it can.

The important part of this commit is a false positive it fixes.

Building a pack from the whole crates.io OSV export and sweeping a
project produced TWO criticals: rustdecimal, correctly, and **tokio
1.38.0**, which is not malware and never has been. The export is 1,524
GHSA and 1,206 RUSTSEC vulnerability advisories against 19 malicious-
package records, and the parser treated all of them as malware.
GHSA-2grh-hm3w-w7hv describes a tokio race condition fixed in 1.8.1;
Hound reported a version released years later as malicious.

Two independent bugs, either of which alone is fatal:

* Vulnerability advisories were ingested at all. A malicious package
  should not exist; a vulnerable one is a legitimate library with a bug
  and most of its versions are fine. Records must now PROVE they are
  malicious-package reports — a MAL- id, the malicious-packages-origins
  marker, or GHSA's "Malicious code in" wording — and anything
  unrecognised is dropped.

* Unrecognised version ranges fell back to "all versions", which is the
  opposite of safe. That is what turned a range of 1.8.0-to-1.8.1 into
  a verdict on every tokio ever published.

Rebuilt against the same input, the pack now holds 19 indicators rather
than 3,614, rustdecimal is still caught and cites MAL-2022-1 rather than
a GHSA advisory, and tokio and serde are clean. The real tokio advisory
is now a regression fixture, because anything that flags tokio is a
product nobody trusts twice.

Also: definitions loading fails CLOSED on authenticity and OPEN on
everything else. No trusted key means no definitions and a message
saying so, because an operator who believes they are protected and is
not is worse off than one who knows. A pack that fails verification is
skipped and the rest still load. No packs at all is a working daemon —
install scripts, prompt injection, pickles and MCP audits need no feed.

There is deliberately no placeholder signing key compiled in. A fake key
that looks real is how a development shortcut becomes a shipped
vulnerability; an empty trust store is noisy in the way that gets fixed
before release. HOUNDD_DEFS_KEY supplies one for development.

294 tests pass across the workspace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 07:29:47 -05:00