#!/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 ) # The desktop app. Optional: a build host without the webkit/gtk # development libraries still produces a working CLI package, it just # does not ship a launcher — which is better than shipping a menu entry # for a binary that is not there. GUI_BIN="$ROOT/gui/src-tauri/target/release/hound-gui" if command -v npx >/dev/null && pkg-config --exists webkit2gtk-4.1 2>/dev/null; then # NOT silenced, and NOT tolerant of failure. Discarding this output once # meant a config error scrolled past unseen and the package shipped the # previous build's binary — the fix looked like it had no effect, twice. ( cd "$ROOT/gui" && npm install --no-audit --no-fund >/dev/null \ && npx tauri build --no-bundle ) fi # The webview loads dist/*.js directly, with no bundler. A bare module # specifier there does not error loudly — it silently fails to resolve and the # window renders its static HTML forever. Catch it here instead of in a bug # report. if grep -rnE '^\s*import .* from "[^./]' "$ROOT/gui/dist"/*.js 2>/dev/null; then echo "ERROR: bare module specifier in the front-end; the webview cannot resolve it" >&2 exit 1 fi if [ -x "$GUI_BIN" ]; then HAVE_GUI=yes echo " including the desktop app" else HAVE_GUI=no echo " NOTE: no GUI binary — packaging the CLI only, and no launcher" fi 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" [ "$HAVE_GUI" = yes ] && install -Dm755 "$GUI_BIN" "$STAGE/usr/bin/hound-gui" # Lets the desktop app elevate a single daemon request through polkit rather # than asking people to open a terminal for every settings change. install -Dm644 "$ROOT/packaging/polkit/com.houndav.hound.policy" \ "$STAGE/usr/share/polkit-1/actions/com.houndav.hound.policy" 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" # A menu entry is a promise that clicking it opens something. It ships # only when the desktop app does, and it launches THAT rather than the # CLI — Exec=hound with Terminal=true opened a terminal, printed help and # exited, which reads to anyone sane as "it does not launch". if [ "$HAVE_GUI" = yes ]; then 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-gui Icon=hound Categories=System;Security; Keywords=antivirus;malware;security;scan;supply chain; Terminal=false StartupWMClass=hound-gui StartupNotify=true DESKTOP fi mkdir -p "$STAGE/DEBIAN" GUI_DEPENDS="" [ "$HAVE_GUI" = yes ] && GUI_DEPENDS=", libwebkit2gtk-4.1-0, libgtk-3-0 | libgtk-3-0t64, libayatana-appindicator3-1" cat > "$STAGE/DEBIAN/control" < Depends: libc6 (>= 2.34)${GUI_DEPENDS} 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 exec-gate on 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 desktop app runs as the logged-in user; the daemon runs as root. # `hound` is how they meet. Membership grants the read side of the API # only — status, scan results, the event log — because quarantine writes # files back out as root and that is not something a group should confer. if ! getent group hound >/dev/null 2>&1; then addgroup --system hound >/dev/null 2>&1 || groupadd -r hound >/dev/null 2>&1 || true fi # Enrol whoever ran the install, since on a desktop that is the person who # will open the app. Group membership only takes effect on their next # login, which is why the notice below says so out loud. ADMIN="${SUDO_USER:-${PKEXEC_UID:-}}" case "$ADMIN" in ''|root) ADMIN="" ;; [0-9]*) ADMIN="$(getent passwd "$ADMIN" | cut -d: -f1)" ;; esac if [ -n "$ADMIN" ] && getent group hound >/dev/null 2>&1; then if ! id -nG "$ADMIN" 2>/dev/null | tr ' ' '\n' | grep -qx hound; then adduser "$ADMIN" hound >/dev/null 2>&1 || usermod -aG hound "$ADMIN" >/dev/null 2>&1 || true ADDED_TO_GROUP=yes fi fi # 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. # Menus cache icons; without this the entry can appear blank until the # user logs out, which is indistinguishable from a broken package. if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -qtf /usr/share/icons/hicolor 2>/dev/null || true fi if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q /usr/share/applications 2>/dev/null || true fi 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 "" if [ "${ADDED_TO_GROUP:-no}" = yes ]; then echo "Added $ADMIN to the 'hound' group so the desktop app can talk to" echo "the daemon. Log out and back in for that to take effect." echo "" fi 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 exec-gate on" 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 # Leave the group behind if anyone is still in it — removing it would # silently strip a gid that could be referenced elsewhere on the system. if getent group hound >/dev/null 2>&1 && [ -z "$(getent group hound | cut -d: -f4)" ]; then delgroup --system hound >/dev/null 2>&1 || groupdel hound >/dev/null 2>&1 || true fi ;; 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/^/ /'