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>
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>
Phase 3's foundation. Three jobs that share a data model:
osv parse the ossf/malicious-packages feed (Apache-2.0, ~226k
records, daily) into indicators
index answer "is this package known bad?" fast enough to ask it
thousands of times per project sweep
pack sign a definitions pack, and verify one before loading it
Validated against the real feed rather than fixtures: the whole
crates.io OSV export, 2,749 records, parsed with zero failures — 3,885
indicators across ten ecosystems, 19 of them MAL-. `rustdecimal` (the
real typosquat of rust_decimal) resolves in crates.io and stays clean
in npm and PyPI, which is the ecosystem isolation working.
Notes on the three:
* A malicious-package record is not a vulnerability record. It almost
always carries introduced:"0" with no fix, meaning EVERY version is
malicious — the package exists only to be malware, so there is no safe
version to upgrade to. Conflating that with a version-bounded
vulnerability either misses real hits or condemns safe versions of
legitimate packages, so the two are modelled separately.
* The index is a cuckoo filter in front of a map. Cuckoo rather than
bloom specifically because a definitions feed needs DELETION: OSV
withdraws records — it once withdrew 157 malware reports after a
false-positive incident — and a filter you cannot remove from means a
withdrawn record costs a probe forever or forces a rebuild. 226k
indicators fit in under 4 MB; the crates.io set is 8 KB.
The property that must never break is no false negatives, and it has
its own test. A false positive costs a hash lookup; a false negative
is malware reported as clean. That is also why a fingerprint hashing
to zero is nudged to one — zero marks an empty slot, so without the
nudge one key in 65,536 would silently vanish.
* Signing is not about entitlement; the subscription gates the server.
It answers "is this pack really from us?", because someone who can
substitute a definitions file can add an entry for /usr/bin/sudo and
have Hound quarantine it on every machine that updates — a supply
chain attack delivered through the security product. Verification
happens on the raw bytes BEFORE anything parses them, so a hostile
pack never reaches the parser at all.
256 tests pass across the workspace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>