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 4's detection core, as a standalone crate. This is the part with no
competitor on Linux, and deliberately the part with no Linux in it —
file parsing and logic only, no fanotify, no /proc, no eBPF — so the
macOS and Windows port is weeks rather than a second product.
Five detectors, 88 tests:
pickle GLOBAL/STACK_GLOBAL walk over .pt/.ckpt/.pkl/.joblib.
torch.load runs a stack machine; a model file is a
program and downloading weights is a code-execution
decision.
injection Instructions aimed at a coding agent in CLAUDE.md,
AGENTS.md, .cursorrules, copilot-instructions.
installscript preinstall/postinstall hooks that curl|sh, decode and
run, reach for credentials, or install persistence.
typosquat Damerau-Levenshtein against popular names, plus
slopsquat detection: new + near-zero downloads + one
edit from something popular is the signature of a name
a model invented and somebody then registered.
mcp Servers fetched unpinned at launch, handed secrets, or
pointed at $HOME or credential paths.
Wired through `supply.sweep` on the socket and `hound supply-chain
<path>`, which exits 1 on a critical so it drops into CI.
Three things worth recording:
* Scoring is by independent category, not by keyword count. One
suspicious phrase is a phrase; two categories at once is an attack.
A file that only says "ignore previous instructions about formatting"
is a warning, not a critical.
* Proximity matters more than presence. The first version flagged an
entirely ordinary conventions file, because it mentioned ".env" in
one paragraph and "prefer small commits" in another. A credential and
a movement verb now have to appear within a sentence of each other.
The test that caught it is kept as the regression.
* Pickle call detection has to come out of the opcode walk, not a byte
search. REDUCE, INST and OBJ are the ASCII letters R, i and o, which
also occur inside every string the stream carries — searching raw
bytes finds the o in "os" and reports a call that never happens,
turning every warning into a critical.
Every finding carries a plain-language explanation and a next step, and
there is a test asserting explanations do not leak rule identifiers or
jargon. The audience includes people who cannot triage a YARA match and
should never be shown one.
Verified against a demo project holding a squatted @vue plugin with a
curl|sh postinstall, a poisoned CLAUDE.md in a vendored repo, an
unpinned MCP server holding a GitHub token, and a pickle calling
os.system — four criticals and one warning, while the legitimate
CLAUDE.md, the real express manifest and the properly-scoped MCP server
beside them stayed clean.
189 tests pass across the workspace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>