Commit graph

7 commits

Author SHA1 Message Date
dev
e92e865ff5 defs: incremental delta feed + free community tier (Option C)
Two changes to how definitions are distributed, sharing one mechanism.

Incremental updates. The old nightly rebuilt the full per-ecosystem pack
every day, so its hash changed and every client re-downloaded 44 MB of npm
daily. Now the server publishes an immutable baseline plus small daily delta
packs (crates/hound-defs/examples/build-delta.rs); the client — which
already fetches only packs whose sha256 it lacks — pulls the baseline once
and then kilobytes a day. When deltas pile up the builder folds them into a
fresh baseline and drops the old files from the index; the client prunes
whatever the index stops listing, so both the server dir and every client's
defs dir stay bounded. Verified end to end: day-2 fetched only the delta
(baseline untouched), day-3 rebaseline pruned the superseded packs.

Free community tier. Free now gets a recent subset of the public OSV feed
(build-community.rs, ~5,000 newest indicators) so a Free install detects
current threats out of the box — not just the heuristics. Pro is the full
235k corpus, daily/near-real-time freshness, and the curated threat pack.
The client always fetches the community channel and gates the full feed +
threat pack on the licence; a lapse prunes both back to exactly what a fresh
Free install has — community pack + built-in rules — while leaving any custom
.yar the user placed themselves untouched. reload_rules() lets a new threat
pack go live without a daemon restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 16:22:39 -05:00
dev
79ea89713e Phase 1: license enforcement, threat pack, Apache-2.0, real Action verification
Make the product buyable and the open-source claim true.

Licence system, end to end. license.rs was well-designed dead code; wire
it up: an Ed25519-signed token (same key and verify-before-parse discipline
as definition packs), `hound license install`, houndd loads and verifies at
boot, and the execution gate and full supply-chain feed now gate on
Capability checks. Verification failing always degrades to Free, never to a
locked-out security tool; an expired licence downgrades with the reason
shown. Adds tools/issue-license.py.

Hound Linux threat pack. 34 curated YARA rules — miners, IoT/DDoS bots,
backdoors, rootkits, ransomware, webshells, droppers, reverse shells —
shipped through a new signed rules-pack channel (.rpack) alongside the
definitions feed. Every rule is ELF- or size-anchored and keyed on
family strings, never syscalls; the builder refuses to sign a pack that
matches a system binary (the goodware gate caught two bad rules), and a
regression test proves every rule fires on a sample and stays quiet on a
document about malware.

Action signature verification. The composite action claimed Ed25519
verification "against the same signed manifest the desktop agent uses" but
only compared a same-host sha256. It now fetches latest.json, verifies the
Ed25519 signature over the canonical release statement against the pinned
release key, and installs the checksum from the verified manifest.

Licence resolved to Apache-2.0: Cargo.toml, a real LICENSE file, README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:51:33 -05:00
dev
4bab6c7a6b 0.1.12: kill the duplicate tray icon, and stop the definition builder from filling the disk
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-21 15:08:35 -05:00
dev
51b8fcd573 0.1.11: one restart mechanism, and a launcher that raises the window
Three tray bugs today, each caused by fixing the previous one without
looking at what else the fix touched. Worth writing down as one thing
rather than three.

**Two dogs, again.** 0.1.4 taught the app to notice its own package
being replaced and reopen. 0.1.5 added the same behaviour to `hound
update`, which is the better mechanism because it works no matter which
version was running — and I did not remove the first. Both fire during
an update: the running app re-execs under a new pid while apt is
working, so the updater kills a pid that has already moved and starts a
second instance beside it.

The in-app restart now waits eight seconds before acting. If the
updater is doing its job this process is terminated during the pause
and never restarts itself; what survives the wait is the only case the
updater cannot cover, which is somebody running `apt upgrade` directly.
Two mechanisms, one arbiter.

**The launcher stopped working.** The single-instance lock added in
0.1.5 to prevent the duplicate icon made a second launch exit quietly,
so clicking the desktop shortcut while Hound was open did nothing at
all — no window, no error, no feedback. It now hands the request to the
running instance, which shows, unminimises and focuses. The same
channel the right-click scan already used: a launch with paths means
"scan these", a launch without means "show yourself".

Also in this release, and the reason the version is what it says it is:
the workspace version did not get bumped. Adding the hound-watch crate
pushed `version =` from line 6 to line 7 of Cargo.toml, and the bump
was a line-numbered sed. The three GUI files went to 0.1.10 and the
workspace stayed at 0.1.9, so the package built as 0.1.9 while the
manifest would have advertised 0.1.10 — every installed agent would
have offered an update forever, installed it, and still seen one
available.

`the_declared_versions_all_agree` caught it before the build finished.
tools/bump-version.sh now does this by matching content and section
rather than line numbers, asserts every substitution actually happened,
and refuses to write invalid JSON.

Adds crates/hound-watch: the registry firehose. npm publishes a CouchDB
_changes stream and PyPI an RSS feed of every new project, both public,
so this needs no user data at all — which is the answer to how a
product with no telemetry grows its detection. Each new package is
handed to the detectors Hound already ships, so there is one definition
of "this install script is hostile" and a rule that fires in CI fires
identically on the firehose. Verified against the live registries: 240
releases in one poll, 60 triaged, parsers tested against captured
responses rather than the network.

476 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:53:18 -05:00
dev
86c33e4648 defs: rebuild and publish the feed daily instead of by hand
Every pack in the published feed was dated 2026.08.21 because that is
when I last ran the ingest by hand. The client-side update machinery
shipped in 0.1.2 works well against a source that never moves, which
means it would have kept every installation confidently green on data
that aged a day for every day that passed — and the staleness warnings
added in the same release would have started telling users the truth
about a problem we caused.

tools/refresh-definitions.sh runs the steps that already existed
(ingest-osv.py, build-pack, index.json) with the properties a published
feed needs:

  - Never publishes an empty feed. If every pack fails to build it
    exits non-zero and leaves the previous one live. An agent that
    installed an empty feed would report a clean machine with no
    indicators loaded, which is worse than one keeping yesterday's.
  - Packs are written before index.json, and each lands via rename.
    The index is what tells an agent a pack exists, so writing it first
    would advertise files that are not there yet, and a rename means a
    fetch mid-run never sees a half-written pack.
  - Old packs stay on disk; the index advertises only the newest per
    ecosystem. An agent that has been offline for a while still has a
    URL that resolves.
  - One ecosystem failing does not cost the others.

The first dry run built a pack called stage-bfsqqa-2026.08.21.pack:
the staging directory was inside the work directory, and the loop
treats every directory in there as an ecosystem. Staging now lives
outside it, and directory names are filtered as well.

The timer runs at 05:20 UTC with Persistent=true, so a builder that was
off does a catch-up rather than silently skipping a day — that being
the exact failure that produces a stale feed nobody notices. The
service runs as the publishing user rather than root: it needs the
signing key and write access to one directory, and a build pipeline
running as root to write a web directory is a bigger target than the
thing it protects.

Verified: the timer is enabled, a real run republished all six packs
and 235,577 indicators, and a client installed from the result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:31:54 -05:00
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
43e3d37b55 defs: 235,577 real indicators, published and fetched over TLS
The machinery has been real for a while and the content was 19 crates.io
records. Now it is the whole malicious-packages feed:

  npm         220,324
  PyPI         11,702
  RubyGems      3,512
  Go               19
  crates.io        19
  Packagist         1
  ────────────────────
              235,577 indicators, 45 MB across six signed packs

The striking number is npm: 220,323 of its 227,149 OSV records are
malicious packages rather than vulnerability advisories. NINETY-SEVEN
PERCENT. That is the whole thesis in one statistic — the dominant
security fact about the npm ecosystem is not that libraries have bugs,
it is that the registry is full of things that exist only to be malware.
It is also why the malicious-versus-vulnerability classifier had to come
first: without it this pack would have been 227,149 indicators, and
7,000 of them would have been ordinary libraries.

Verified end to end from a genuinely empty definitions directory:

  before:  defs: no verified packs were found
  update:  6 pack(s) installed, 0 already current
           loaded 235577 indicators from 6 pack(s) [2026.08.21]
  elapsed: 1.4 seconds for 45 MB, verified and loaded
  RSS:     214 MB (unit cap is 1G)

And it detects. A lockfile with 500 ordinary packages and three real
malicious ones — ineldua, dian-kue20-riris, restart-rocket-jasmine-jwt,
picked at random from the feed rather than chosen to work — produced
three criticals and no false positives, in 11 milliseconds. The cuckoo
filter is doing exactly what it was built for: 500 clean lookups never
touch the map.

tools/ingest-osv.py does the coarse filter so build-pack does not walk a
quarter of a million files it will discard. It mirrors the Rust
classifier deliberately, and the Rust side still applies its own — a
record slipping through the Python changes nothing.

`hound status` now reports definitions, and reports them when there are
none, with the reason. Silence would let somebody believe they were
protected by a feed they never received.

Known and not yet solved: every agent downloads 43 MB of npm pack on
first update, and the whole pack again whenever it changes. Incremental
updates and a CDN in front of defs.houndav.com are both wanted. The
subdomain split exists precisely so the second one is a DNS change.

362 tests pass.

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