Commit graph

4 commits

Author SHA1 Message Date
dev
46c90b0f59 0.1.9: container config, disabled TLS, and injection inside dependencies
Seven detections, all under the free tier. The line Joe and I settled
on is scope rather than capability: an individual protecting their own
machine gets every check at full depth, and what is sold is the same
protection made continuous, enforced, and shared across a team.

**Containers.** The Docker socket is the one that matters. Mounting
/var/run/docker.sock into a container is not access to Docker, it is
root on the host — anything that can talk to that socket can start a
privileged container with the host filesystem mounted — and the
explanation says exactly that. Also privileged: true, host networking,
bind mounts of /, /etc, ~/.ssh and ~/.aws, secrets baked into image
layers (with the part people learn too late: docker history keeps them
after a later instruction deletes them), curl piped into a shell during
a build nobody watches, ADD from a URL, running as root, and COPY . .
with no .dockerignore shipping the .env and the whole .git directory
into a published image.

**Disabled certificate checking**, across nine ecosystems. Reported as
a warning rather than a critical because it is often deliberate, and
skipped entirely in test files — turning verification off in a fixture
is normal, and flagging it there is how a check gets switched off
wholesale. The explanation leads with the trap: the connection still
looks encrypted.

**Prompt injection inside dependencies.** Hound already read the
project's own CLAUDE.md; an assistant working in a repository reads far
more than that, including the README of every package it touches. All
of that is attacker-controlled text, and publishing a package whose
README addresses the assistant instead of the reader costs nothing to
try. The finding names the package, including scoped ones, and resolves
nested dependencies to the innermost package — the one that actually
shipped the file.

An injected instruction file inside a dependency used to produce two
findings, one from each check. It reports once now, as the dependency
finding: "the package awesome-agents ships an injection" is something a
person can act on, and "there is an injection in this file" is not.

**CI beyond GitHub** — GitLab, Jenkins, CircleCI, Bitbucket, Azure,
Drone. The GitLab equivalent of pull_request_target requires both merge
request pipelines and a protected-variable reference before it reports;
merge request pipelines alone are how everybody uses GitLab, and
flagging them would fire on nearly every project.

Also adds crates/hound-api/src/license.rs: an entitlement check so
nothing ships untiered by accident. It does not try to stop anybody —
the binary is Apache-2.0 and the check can be deleted — and there is no
phone-home, so a machine with no network still knows what it bought and
we never learn where our software runs. An expired licence falls back
to Free rather than failing closed: somebody whose card lapsed must not
end up with less protection than a stranger who installed Hound this
morning, and there is a test for it. Every message about an absent
capability has to name what still works, which is also tested.

466 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:37:36 -05:00
dev
aae41a9371 0.1.6: security hygiene, and right-click scanning
Malware scanning asks whether a file is hostile. The check that
actually loses people their accounts is a different one: what has
already been exposed, and what is about to be? crates/hound-supply/src/
hygiene.rs answers it, and runs as part of every project sweep.

  - A secret file tracked by git. The emergency case: it is in the
    history, in every clone, and in every fork. The advice says rotate
    BEFORE `git rm --cached`, because removing a pushed secret does not
    un-share it, and a test asserts that ordering.
  - Credentials hardcoded in source, recognised by issuer format —
    AWS, GitHub, Anthropic, OpenAI, Stripe, Slack, GitLab, npm, PyPI,
    Google, and the PEM private-key headers.
  - Secret files readable by every account on the machine.
  - Secret files with nothing in .gitignore covering them: the near
    miss that the next `git add -A` turns into the emergency above.
  - GitHub Actions: pull_request_target with a checkout of the pull
    request (a stranger's code, your secrets, your write token), a
    secret echoed into the build log, a downloaded script piped into a
    shell, and third-party actions on a moving tag.

Two rules govern all of it. **Findings are actionable**: no entropy
heuristics, because "high entropy string" is a coin flip a human then
has to adjudicate, and people stop reading after the second false
alarm. Every detector recognises a documented credential format or
reports a structural fact that is true or false. **Nothing secret is
copied into a finding** — a report naming the key it found has moved
the key into a log, a CI artefact, or an assistant's context window,
which is the thing being prevented. There is a test for that.

Tracked-file status comes from parsing .git/index rather than running
git: the sweep is pointed at repositories precisely because they are
not trusted, and starting a subprocess inside one is what a hostile
repository wants.

Against a deliberately bad test repository: four critical, five
warnings, and correctly silent on .env.example and actions/checkout@v4
— flagging those is how a scanner gets ignored.

Also in this release:

  - Right-click "Scan for Threats with Hound" in Nemo, Caja and
    Dolphin, whose menu entries are system files. GNOME Files and
    Thunar keep theirs per-user, so `hound context-menu install`
    handles those. The icon is symbolic, so the file manager recolours
    it to the menu's own theme instead of dropping a violet dog into a
    monochrome menu.
  - A right-click while the app is already open hands the request to
    the running instance rather than refusing. A menu item that
    silently does nothing because the app happens to be open is
    indefensible.
  - Two fixes for the duplicate tray icon. The updater slept a fixed
    600ms after SIGTERM and then started the replacement; if the old
    process outlived that, the panel kept its item and the result was
    two dogs, the older of which could not be clicked or closed because
    nothing was behind it. It now waits for the process to actually
    leave /proc, escalating to SIGKILL after five seconds. And the app
    itself now holds an advisory lock for its lifetime, so a second
    instance cannot exist — the kernel releases the lock however the
    process dies, so a stale one is not a state that can happen.

402 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:15:35 -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
Hound
6beb73771a hound-supply: the supply-chain and agent-era scanner
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>
2026-08-20 23:48:48 -05:00