Four tools over MCP stdio, so a coding assistant can ask Hound about a
project at the moment it matters rather than after:
hound_check_project the full supply-chain sweep
hound_check_package is this package known bad, before installing it
hound_check_file one file: a model, a lockfile, a manifest
hound_check_mcp_config audit the servers your assistant already trusts
READ-ONLY, permanently. There is no hound_quarantine, no hound_delete,
no way to change a setting. An agent can be persuaded by the very
repository it is inspecting — that is the threat this product exists to
detect — so a destructive MCP tool would hand the attacker exactly the
capability they were reaching for. A test asserts every tool name starts
with hound_check_ and contains none of quarantine/delete/remove/restore/
settings/set/update/install/write/exec, so the property cannot erode.
The output is written to be read twice: by the model that called the
tool, and by the human reading that model's summary. That rules out rule
identifiers and jargon — a test greps the output for both — and it rules
out ambiguity about severity. A model reading "1 warning" may well decide
to proceed, so a critical finding leads with an explicit recommendation
and names the consequence: this runs code during installation, before
any of the project's own code runs.
Clean results say what they did NOT check. "Nothing wrong" that reads as
a blanket endorsement is worse than no answer, so a clean project notes
it is not a source review, a clean name check notes it did not read the
package's contents, and a clean MCP audit still points out that every
server listed runs with your permissions and is called without asking.
The server passes its own audit, which was the point:
{ "mcpServers": { "hound": { "command": "/usr/bin/hound-mcp" } } }
No npx, so nothing is fetched at launch. No env, so no secret is handed
over. No path argument, so it is granted no directory. Hound's MCP audit
flags all three in other people's configs; a security tool that failed
its own check would have answered the only question that mattered.
Verified against the installed binary.
Protocol notes, since both are easy to get wrong and fatal:
- a notification carries no id and must never be answered; MCP sends
notifications/initialized straight after the handshake, so replying
corrupts the stream on the first exchange
- an id of 0 is still an id, and treating it as absent silently drops
the first call from any client that counts from zero
- a tool failure is a RESULT with isError, not a JSON-RPC error: the
agent should see "I could not read that path" as an answer it can act
on, not a transport fault that looks like a broken server
- nothing but protocol messages ever goes to stdout; diagnostics go to
stderr, because one stray println corrupts the session
Shipped in the .deb, and the postinstall now prints the config entry.
334 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
191 lines
6.3 KiB
Bash
Executable file
191 lines
6.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# Build a .deb for Ubuntu / Debian / Linux Mint.
|
|
#
|
|
# Deliberately hand-rolled rather than cargo-deb: the package needs a
|
|
# postinst that creates the vault with the right mode, a conffile that
|
|
# survives upgrades, and a unit that is enabled but whose gate stays off
|
|
# until the operator turns it on. That is easier to read as a script than
|
|
# as a pile of metadata, and it is the thing most likely to need auditing.
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
VERSION="$(grep -m1 '^version' "$ROOT/Cargo.toml" | cut -d'"' -f2)"
|
|
ARCH="$(dpkg --print-architecture)"
|
|
OUT="${OUT:-$ROOT/dist}"
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
# mktemp -d creates 0700, and dpkg applies the staging root's mode to "/".
|
|
# Installing this package would chmod / to 0700 and break the machine.
|
|
chmod 0755 "$STAGE"
|
|
|
|
echo "building hound ${VERSION} (${ARCH})"
|
|
( cd "$ROOT" && cargo build --release -p houndd -p hound -p hound-mcp )
|
|
|
|
install -Dm755 "$ROOT/target/release/houndd" "$STAGE/usr/bin/houndd"
|
|
install -Dm755 "$ROOT/target/release/hound" "$STAGE/usr/bin/hound"
|
|
install -Dm755 "$ROOT/target/release/hound-mcp" "$STAGE/usr/bin/hound-mcp"
|
|
install -Dm644 "$ROOT/packaging/systemd/houndd.service" \
|
|
"$STAGE/lib/systemd/system/houndd.service"
|
|
install -Dm644 "$ROOT/crates/houndd/rules/hound-builtin.yar" \
|
|
"$STAGE/usr/share/hound/rules/hound-builtin.yar"
|
|
install -Dm644 "$ROOT/README.md" "$STAGE/usr/share/doc/hound/README.md"
|
|
|
|
# Launcher icon: the white mark on a periwinkle tile (app-*.png), not the
|
|
# bare brand mark. The tray ladder is a different family and ships with
|
|
# the GUI, because tray glyphs must stay transparent to sit on any panel.
|
|
for size in 16 22 24 32 48 64 128 256 512; do
|
|
src="$ROOT/assets/icons/app-${size}.png"
|
|
[ -f "$src" ] && install -Dm644 "$src" \
|
|
"$STAGE/usr/share/icons/hicolor/${size}x${size}/apps/hound.png"
|
|
done
|
|
install -Dm644 "$ROOT/assets/icons/hound-app.svg" \
|
|
"$STAGE/usr/share/icons/hicolor/scalable/apps/hound.svg"
|
|
|
|
install -Dm644 /dev/stdin "$STAGE/usr/share/applications/hound.desktop" <<'DESKTOP'
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=Hound Antivirus
|
|
GenericName=Antivirus
|
|
Comment=Endpoint and supply-chain protection for Linux
|
|
Exec=hound
|
|
Icon=hound
|
|
Categories=System;Security;Utility;
|
|
Keywords=antivirus;malware;security;scan;supply chain;
|
|
Terminal=true
|
|
DESKTOP
|
|
|
|
mkdir -p "$STAGE/DEBIAN"
|
|
|
|
cat > "$STAGE/DEBIAN/control" <<CONTROL
|
|
Package: hound
|
|
Version: ${VERSION}
|
|
Section: utils
|
|
Priority: optional
|
|
Architecture: ${ARCH}
|
|
Maintainer: Hound <support@houndav.com>
|
|
Depends: libc6 (>= 2.34)
|
|
Suggests: clamav-daemon
|
|
Homepage: https://houndav.com
|
|
Description: Hound Antivirus for Linux
|
|
Endpoint and supply-chain protection built for the distributions people
|
|
actually run. Scanning is yara-x in process; real-time protection uses
|
|
fanotify, so a binary can be refused at execve rather than reported
|
|
after it has already run.
|
|
.
|
|
The execution gate is installed switched OFF. It needs CAP_SYS_ADMIN and
|
|
covers the whole root filesystem, so turning it on is the operator's
|
|
decision: hound settings set exec_gate true
|
|
CONTROL
|
|
|
|
cat > "$STAGE/DEBIAN/conffiles" <<'CONFFILES'
|
|
/etc/hound/hound.toml
|
|
CONFFILES
|
|
|
|
install -Dm644 /dev/stdin "$STAGE/etc/hound/hound.toml" <<'CONF'
|
|
# Hound Antivirus configuration.
|
|
#
|
|
# Live settings are managed through `hound settings` and stored per user;
|
|
# this file holds the machine-wide defaults the daemon starts from.
|
|
|
|
# Deny execution until a verdict is returned. Needs CAP_SYS_ADMIN.
|
|
# Off by default: it covers the whole root filesystem, and that is the
|
|
# operator's call to make rather than the installer's.
|
|
exec_gate = false
|
|
|
|
# Mounts the gate covers. Empty means the root filesystem.
|
|
exec_gate_paths = []
|
|
|
|
# Never held for a verdict.
|
|
exclude_paths = ["/proc", "/sys", "/dev", "/run", "/var/lib/docker"]
|
|
|
|
# Files larger than this are allowed through unread.
|
|
max_file_size_mb = 100
|
|
|
|
# "quarantine" or "alert".
|
|
on_detect = "quarantine"
|
|
CONF
|
|
|
|
cat > "$STAGE/DEBIAN/postinst" <<'POSTINST'
|
|
#!/bin/sh
|
|
set -e
|
|
|
|
case "$1" in
|
|
configure)
|
|
# The vault holds live malware: root-only, and on a filesystem where
|
|
# nothing in it can be executed even by accident.
|
|
mkdir -p /var/lib/hound/vault /var/lib/hound/rules /var/log/hound
|
|
chmod 0700 /var/lib/hound/vault
|
|
chmod 0755 /var/lib/hound /var/lib/hound/rules
|
|
chmod 0750 /var/log/hound
|
|
|
|
# The built-in rules are compiled INTO the binary; /var/lib/hound/rules
|
|
# is for additional packs only. Copying the built-ins there made the
|
|
# daemon compile them twice and log a duplicate-declaration error on
|
|
# every start. The copy under /usr/share is documentation, not input.
|
|
|
|
if [ -d /run/systemd/system ]; then
|
|
systemctl daemon-reload || true
|
|
systemctl enable houndd.service || true
|
|
systemctl restart houndd.service || true
|
|
fi
|
|
|
|
echo ""
|
|
echo "Hound is installed and scanning on demand."
|
|
echo ""
|
|
echo " hound status what the daemon sees"
|
|
echo " hound scan ~/Downloads scan a directory"
|
|
echo ""
|
|
echo "Real-time execution blocking is OFF until you turn it on:"
|
|
echo ""
|
|
echo " sudo hound settings set exec_gate true"
|
|
echo ""
|
|
echo "To let a coding assistant check repositories before trusting them,"
|
|
echo "add this to its MCP configuration:"
|
|
echo ""
|
|
echo ' { "mcpServers": { "hound": { "command": "/usr/bin/hound-mcp" } } }'
|
|
echo ""
|
|
;;
|
|
esac
|
|
exit 0
|
|
POSTINST
|
|
|
|
cat > "$STAGE/DEBIAN/prerm" <<'PRERM'
|
|
#!/bin/sh
|
|
set -e
|
|
case "$1" in
|
|
remove|deconfigure)
|
|
if [ -d /run/systemd/system ]; then
|
|
systemctl stop houndd.service || true
|
|
systemctl disable houndd.service || true
|
|
fi
|
|
;;
|
|
esac
|
|
exit 0
|
|
PRERM
|
|
|
|
cat > "$STAGE/DEBIAN/postrm" <<'POSTRM'
|
|
#!/bin/sh
|
|
set -e
|
|
case "$1" in
|
|
purge)
|
|
# The vault is deliberately NOT removed on `remove`, only on `purge`,
|
|
# and even then only after saying so: it may be the sole copy of
|
|
# evidence somebody still needs.
|
|
echo "Removing the Hound quarantine vault at /var/lib/hound/vault"
|
|
rm -rf /var/lib/hound /var/log/hound
|
|
;;
|
|
esac
|
|
if [ -d /run/systemd/system ]; then
|
|
systemctl daemon-reload || true
|
|
fi
|
|
exit 0
|
|
POSTRM
|
|
|
|
chmod 0755 "$STAGE/DEBIAN/postinst" "$STAGE/DEBIAN/prerm" "$STAGE/DEBIAN/postrm"
|
|
|
|
mkdir -p "$OUT"
|
|
DEB="$OUT/hound_${VERSION}_${ARCH}.deb"
|
|
fakeroot dpkg-deb --build --root-owner-group "$STAGE" "$DEB" >/dev/null
|
|
echo "built $DEB"
|
|
dpkg-deb -I "$DEB" | sed 's/^/ /'
|