packaging: .deb, AppImage, rpm spec, PKGBUILD, hardened unit, app icon

Phase 2. "Distro-agnostic" was a claim with no packaging behind it.

.deb and AppImage are both built and tested here; the rpm spec and
PKGBUILD are written but not yet built, since neither rpmbuild nor
makepkg exists on this machine.

The AppImage is deliberately not a packaged daemon. An AppImage is
unprivileged by design — no install, no root, no systemd — so the
execution gate cannot exist in one, and pretending otherwise would be
worse than saying so. What it is instead is the try-before-you-install
build: on-demand scanning, quarantine under the user's own data dir,
rootkit and supply-chain checks, all with nothing left behind. Asking
it for the gate prints why it cannot and how to get it. Verified with
an isolated HOME: status, scan, EICAR caught, gate refusal.

Two packaging bugs caught by looking rather than assuming:

* mktemp -d creates 0700 and dpkg applies the staging root's mode to
  "/". Installing that package would have chmodded the root directory
  to 0700 and broken the machine.
* AppRun wrote its log before creating the directory, and built a
  socket path that can exceed sun_path (108 bytes) when
  XDG_RUNTIME_DIR is long. Both fixed; the socket falls back rather
  than failing with an error nobody can act on.

The systemd unit is hardened as far as this daemon can be. Notably
ProtectSystem=strict is WRONG here and is left off on purpose: it
remounts everything read-only, and quarantine has to remove a threat
from wherever it landed. ProtectHome is off for the same reason. The
CapabilityBoundingSet mirrors what caps.rs drops to, so the machine is
protected even if the binary is replaced by one that does not reduce
itself.

App icon: the mark in white on a periwinkle tile, per Joe. Small sizes
are not the same artwork scaled down — the mark is line-weight, so at
16px a 62% inset leaves about a pixel and a half of stroke and the head
turns to mush. Each size is authored with its own inset and corner
radius, and the ground goes flat below 32px because a gradient across
16 pixels is just noise. The tray ladder is untouched: those glyphs stay
transparent and re-tint per state so they can sit on any panel colour.

Package installs are NOT enabled by default beyond the daemon itself —
exec_gate stays off until the operator turns it on, in every packaging
format.

99 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hound 2026-08-20 23:37:17 -05:00
parent 85234842f9
commit 6016e1b4ea
21 changed files with 534 additions and 0 deletions

BIN
assets/icons/app-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
assets/icons/app-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

BIN
assets/icons/app-22.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

BIN
assets/icons/app-24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
assets/icons/app-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

BIN
assets/icons/app-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icons/app-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/icons/app-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/icons/app-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Hound app icon: the dog mark in white on a periwinkle tile.
Distinct from the tray ladder (state-*.png), whose glyphs stay
transparent and re-tint per security state so they can sit on any
panel colour. This one is the launcher, window and About icon.
Small sizes are NOT this artwork scaled down. The mark is line-weight,
so at 16px a 62% inset leaves roughly a pixel and a half of stroke and
the head becomes mush. Each size in the family is authored with its own
inset and corner radius — the mark grows as the canvas shrinks.
-->
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<title>Hound Antivirus</title>
<defs>
<linearGradient id="ground" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#A9A7E8"/>
<stop offset="1" stop-color="#8A88D8"/>
</linearGradient>
</defs>
<rect x="0" y="0" width="512" height="512" rx="114.7" ry="114.7" fill="url(#ground)"/>
<g transform="translate(97.28,97.28) scale(11.33714)" fill="#FFFFFF" fill-rule="nonzero">
<path d="M3.80069371,23.1130888 C4.43848107,23.5650959 5.14832497,23.919844 5.9089202,24.1573863 L4.47060635,26.3139954 C4.11309659,26.8500464 3.3887224,26.9947828 2.85267132,26.637273 C2.35490959,26.3052997 2.19455701,25.6569993 2.46043143,25.1368862 L2.52939365,25.019338 L3.80069371,23.1130888 Z M14.5833333,1.16666667 C16.7811895,1.16666667 18.6893658,2.52489596 19.4646473,4.47968116 L19.5335,4.66666667 L24.3833333,4.66666667 C25.0448446,4.66666667 25.5894608,5.16717524 25.6591362,5.81016498 L25.6666667,5.94999885 L25.6666667,11.375 C25.6666667,14.0328704 23.5754085,16.2023012 20.9485691,16.3276156 L20.7083333,16.3333333 L18.7961667,16.3321667 L18.8104203,16.3635263 C19.2754933,17.3735749 19.6443042,18.3167095 19.9164818,19.1943797 L20.0445161,19.627766 C20.7657518,22.2003082 19.7286621,25.1941618 17.5638169,26.637392 C17.0277001,26.9948032 16.3033525,26.8499337 15.9459413,26.3138169 C15.5885301,25.7777001 15.7333996,25.0533525 16.2695164,24.6959413 C17.5764793,23.8246327 18.2515149,21.8759509 17.7978097,20.2576506 C17.4455369,19.0011449 16.8372737,17.5384042 15.9713012,15.8761384 C15.8759396,15.6930882 15.8261459,15.4897339 15.8261459,15.2833333 C15.8261459,14.6231337 16.3245332,14.0799436 16.9690039,14.0080605 L17.1092123,14 L20.7083333,14 C22.0976747,14 23.2349191,12.9206448 23.3272774,11.5547236 L23.3333333,11.375 L23.3333333,6.99883333 L17.6136472,7.00006824 L17.4704091,5.99857195 C17.2758266,4.63808519 16.1483073,3.59829995 14.780321,3.50658494 L14.5833333,3.5 L12.7831667,3.5 L12.5783017,3.50389564 C8.27042644,3.63569563 4.8164366,7.09906173 4.67140846,11.3859455 L4.66666667,11.6666667 L4.66631609,16.8880679 L4.67097109,17.092305 C4.76225687,18.9495018 6.29888585,20.4166667 8.16666667,20.4166667 C10.0996633,20.4166667 11.6666667,18.8496633 11.6666667,16.9166667 C11.6666667,16.1268183 11.523797,15.416107 11.2062777,14.4675176 L10.7570788,13.2056109 C10.1975498,11.6462738 9.90930052,10.544355 9.77521466,9.11410583 C9.71507223,8.47258663 10.1863712,7.90377749 10.8278904,7.84363507 C11.4694096,7.78349264 12.0382188,8.25479164 12.0983612,8.89631084 C12.1996371,9.97658728 12.4067419,10.8376191 12.8110905,12.0132787 L13.3678001,13.5759215 L13.4189446,13.7268763 C13.8098537,14.8947176 14,15.8406089 14,16.9166667 C14,20.1383277 11.3883277,22.75 8.16666667,22.75 C5.13975753,22.75 2.63542176,20.439154 2.35770657,17.4371488 L2.3393951,17.1781792 L2.33333333,16.9166667 L2.33333333,11.6666667 C2.33333333,6.09263102 6.68344803,1.51408374 12.2162292,1.1851268 L12.4308333,1.17483333 L12.4315796,1.16666667 L14.5833333,1.16666667 Z M16.1504743,6.81070338 L16.4543586,8.53411695 C16.5662457,9.1686603 16.1425491,9.77376175 15.5080058,9.88564887 C14.8734624,9.99753598 14.268361,9.57383938 14.1564739,8.93929603 L13.8525895,7.21588246 C13.7407024,6.58133911 14.164399,5.97623765 14.7989424,5.86435054 C15.4334857,5.75246343 16.0385872,6.17616002 16.1504743,6.81070338 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
dist/Hound-0.1.0-x86_64.AppImage vendored Executable file

Binary file not shown.

BIN
dist/hound_0.1.0_amd64.deb vendored Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

46
packaging/aur/PKGBUILD Normal file
View file

@ -0,0 +1,46 @@
# Maintainer: Hound <support@houndav.com>
pkgname=hound
pkgver=0.1.0
pkgrel=1
pkgdesc="Endpoint and supply-chain protection for Linux"
arch=('x86_64' 'aarch64')
url="https://houndav.com"
license=('Apache-2.0')
depends=('systemd-libs')
optdepends=('clamav: the Windows-malware corpus, for the file-server carrier case')
makedepends=('rust>=1.91' 'cargo')
backup=('etc/hound/hound.toml')
install=hound.install
source=("$pkgname-$pkgver.tar.gz::https://git.joelovestech.com/Hound/Antivirus/archive/v$pkgver.tar.gz")
sha256sums=('SKIP')
build() {
cd "$srcdir/Antivirus"
cargo build --release --locked -p houndd -p hound
}
check() {
cd "$srcdir/Antivirus"
# The goodware gate runs here too: a rule that fires on an Arch system
# binary must fail the build on Arch, not only on Debian.
cargo test --release --locked --workspace
}
package() {
cd "$srcdir/Antivirus"
install -Dm755 target/release/houndd "$pkgdir/usr/bin/houndd"
install -Dm755 target/release/hound "$pkgdir/usr/bin/hound"
install -Dm644 packaging/systemd/houndd.service \
"$pkgdir/usr/lib/systemd/system/houndd.service"
install -Dm644 crates/houndd/rules/hound-builtin.yar \
"$pkgdir/usr/share/hound/rules/hound-builtin.yar"
install -dm755 "$pkgdir/var/lib/hound/rules"
install -dm700 "$pkgdir/var/lib/hound/vault"
for s in 16 22 24 32 48 64 128 256 512; do
install -Dm644 "assets/icons/app-${s}.png" \
"$pkgdir/usr/share/icons/hicolor/${s}x${s}/apps/hound.png"
done
install -Dm644 assets/icons/hound-app.svg \
"$pkgdir/usr/share/icons/hicolor/scalable/apps/hound.svg"
install -Dm644 README.md "$pkgdir/usr/share/doc/hound/README.md"
}

136
packaging/build-appimage.sh Executable file
View file

@ -0,0 +1,136 @@
#!/usr/bin/env bash
#
# Build the Hound AppImage.
#
# What an AppImage can and cannot be, for this product:
#
# An AppImage is unprivileged by design — no install, no root, no
# systemd. The execution gate needs CAP_SYS_ADMIN and a filesystem-wide
# fanotify mark, so it is simply not available here, and pretending
# otherwise would be worse than saying so.
#
# What IS available is everything that does not need privilege:
# on-demand scanning, the quarantine vault under the user's own data
# directory, rootkit heuristics, supply-chain checks and the CLI. That
# makes this the "try it without installing anything" build, and the
# AppRun below says exactly that when the gate is asked for.
#
# Needs appimagetool on PATH (or at $APPIMAGETOOL).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
VERSION="$(grep -m1 '^version' "$ROOT/Cargo.toml" | cut -d'"' -f2)"
OUT="${OUT:-$ROOT/dist}"
TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
APPDIR="$(mktemp -d)/Hound.AppDir"
trap 'rm -rf "$(dirname "$APPDIR")"' EXIT
if [ -z "$TOOL" ]; then
echo "appimagetool not found. Set APPIMAGETOOL=/path/to/appimagetool" >&2
exit 2
fi
echo "building Hound AppImage ${VERSION}"
( cd "$ROOT" && cargo build --release -p houndd -p hound )
mkdir -p "$APPDIR"
chmod 0755 "$APPDIR"
install -Dm755 "$ROOT/target/release/hound" "$APPDIR/usr/bin/hound"
install -Dm755 "$ROOT/target/release/houndd" "$APPDIR/usr/bin/houndd"
install -Dm644 "$ROOT/crates/houndd/rules/hound-builtin.yar" \
"$APPDIR/usr/share/hound/rules/hound-builtin.yar"
# The launcher icon is the white mark on periwinkle, sized optically.
install -Dm644 "$ROOT/assets/icons/app-256.png" "$APPDIR/hound.png"
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" \
"$APPDIR/usr/share/icons/hicolor/${size}x${size}/apps/hound.png"
done
install -Dm644 "$ROOT/assets/icons/hound-app.svg" \
"$APPDIR/usr/share/icons/hicolor/scalable/apps/hound.svg"
cat > "$APPDIR/hound.desktop" <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=Hound Antivirus
Comment=Endpoint and supply-chain protection for Linux
Exec=hound
Icon=hound
Categories=System;Security;
Terminal=true
DESKTOP
cat > "$APPDIR/AppRun" <<'APPRUN'
#!/bin/sh
#
# Portable-mode launcher.
#
# Everything lives under the user's own directories, so the AppImage
# leaves nothing behind on the system and needs no privilege. The one
# thing it cannot do is gate execution — see below.
set -e
HERE="$(dirname "$(readlink -f "$0")")"
export PATH="$HERE/usr/bin:$PATH"
# Rules ship inside the bundle; point the daemon at them read-only.
export HOUNDD_RULES_DIR="${HOUNDD_RULES_DIR:-$HERE/usr/share/hound/rules}"
# Keep state in the user's own dirs rather than /var/lib.
export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
export HOUNDD_SOCK="${HOUNDD_SOCK:-${XDG_RUNTIME_DIR:-/tmp}/houndd.sock}"
mkdir -p "$XDG_DATA_HOME/hound" "$XDG_CONFIG_HOME/hound"
# A Unix socket path cannot exceed sun_path (108 bytes on Linux), and
# XDG_RUNTIME_DIR is not always short. Fall back rather than failing with
# an error most people cannot act on.
if [ "${#HOUNDD_SOCK}" -ge 100 ]; then
HOUNDD_SOCK="/tmp/houndd-$(id -u).sock"
export HOUNDD_SOCK
fi
# Start a private daemon if one is not already answering.
if ! "$HERE/usr/bin/hound" status >/dev/null 2>&1; then
"$HERE/usr/bin/houndd" >"${XDG_DATA_HOME}/hound/appimage.log" 2>&1 &
# Wait for the socket rather than sleeping a fixed amount.
i=0
while [ ! -S "$HOUNDD_SOCK" ] && [ $i -lt 50 ]; do
i=$((i + 1))
sleep 0.1
done
fi
case "${1:-}" in
settings)
case "${2:-} ${3:-}" in
"set exec_gate")
cat >&2 <<'MSG'
The execution gate is not available in the AppImage.
Blocking a program at execve needs CAP_SYS_ADMIN and a filesystem-wide
fanotify mark, which an unprivileged, uninstalled bundle cannot have.
Everything else works here: on-demand scanning, quarantine, rootkit
checks and supply-chain checks.
For real-time protection, install the package:
sudo apt install ./hound_*.deb
sudo hound settings set exec_gate true
MSG
exit 2
;;
esac
;;
esac
exec "$HERE/usr/bin/hound" "$@"
APPRUN
chmod 0755 "$APPDIR/AppRun"
mkdir -p "$OUT"
ARCH=x86_64 "$TOOL" --no-appstream "$APPDIR" "$OUT/Hound-${VERSION}-x86_64.AppImage" 2>&1 \
| grep -vE "^(WARNING|Warning)" || true
echo "built $OUT/Hound-${VERSION}-x86_64.AppImage"

186
packaging/build-deb.sh Executable file
View file

@ -0,0 +1,186 @@
#!/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 )
install -Dm755 "$ROOT/target/release/houndd" "$STAGE/usr/bin/houndd"
install -Dm755 "$ROOT/target/release/hound" "$STAGE/usr/bin/hound"
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)
Recommends: 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
# Seed the built-in rules where the daemon looks for packs, so an
# offline install still detects something.
if [ -f /usr/share/hound/rules/hound-builtin.yar ]; then
cp -n /usr/share/hound/rules/hound-builtin.yar /var/lib/hound/rules/ || 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 ""
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 ""
;;
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/^/ /'

70
packaging/rpm/hound.spec Normal file
View file

@ -0,0 +1,70 @@
Name: hound
Version: 0.1.0
Release: 1%{?dist}
Summary: Hound Antivirus for Linux
License: Apache-2.0
URL: https://houndav.com
Source0: %{name}-%{version}.tar.gz
BuildRequires: rust >= 1.91, cargo, systemd-rpm-macros
Requires: systemd
Recommends: clamd
%description
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 enabling it is the operator's call:
hound settings set exec_gate true
%prep
%autosetup
%build
cargo build --release -p houndd -p hound
%install
install -Dm755 target/release/houndd %{buildroot}%{_bindir}/houndd
install -Dm755 target/release/hound %{buildroot}%{_bindir}/hound
install -Dm644 packaging/systemd/houndd.service \
%{buildroot}%{_unitdir}/houndd.service
install -Dm644 crates/houndd/rules/hound-builtin.yar \
%{buildroot}%{_datadir}/hound/rules/hound-builtin.yar
install -dm755 %{buildroot}%{_sharedstatedir}/hound/rules
install -dm700 %{buildroot}%{_sharedstatedir}/hound/vault
for s in 16 22 24 32 48 64 128 256 512; do
install -Dm644 assets/icons/app-${s}.png \
%{buildroot}%{_datadir}/icons/hicolor/${s}x${s}/apps/hound.png
done
install -Dm644 assets/icons/hound-app.svg \
%{buildroot}%{_datadir}/icons/hicolor/scalable/apps/hound.svg
%post
# Seed the built-in rules so an offline install still detects something.
cp -n %{_datadir}/hound/rules/hound-builtin.yar \
%{_sharedstatedir}/hound/rules/ 2>/dev/null || :
%systemd_post houndd.service
%preun
%systemd_preun houndd.service
%postun
%systemd_postun_with_restart houndd.service
%files
%{_bindir}/houndd
%{_bindir}/hound
%{_unitdir}/houndd.service
%{_datadir}/hound/rules/hound-builtin.yar
%{_datadir}/icons/hicolor/*/apps/hound.png
%{_datadir}/icons/hicolor/scalable/apps/hound.svg
%dir %{_sharedstatedir}/hound
%dir %{_sharedstatedir}/hound/rules
# The vault holds live malware: root-only, never world-readable.
%attr(0700,root,root) %dir %{_sharedstatedir}/hound/vault
%changelog
* Fri Aug 21 2026 Hound <support@houndav.com> - 0.1.0-1
- yara-x engine in process; fanotify execution gate

View file

@ -0,0 +1,70 @@
[Unit]
Description=Hound Antivirus daemon
Documentation=https://houndav.com/docs
After=network.target
# The gate is only useful while the filesystem it watches is mounted.
RequiresMountsFor=/var/lib/hound
[Service]
Type=exec
ExecStart=/usr/bin/houndd
Restart=on-failure
RestartSec=2s
# ── Privilege ────────────────────────────────────────────────────────
#
# houndd reduces its own capabilities at startup (see crates/houndd/src/
# caps.rs). This bounding set is the second half of that: it protects the
# machine if the binary is ever replaced by one that does not, and it
# means the daemon never *holds* the rest even momentarily.
#
# It cannot be narrower. fanotify needs SYS_ADMIN; scanning arbitrary
# files needs DAC_READ_SEARCH; quarantining out of a directory owned by
# someone else needs DAC_OVERRIDE; stripping the execute bit off a file
# we do not own needs FOWNER.
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_FOWNER
AmbientCapabilities=CAP_SYS_ADMIN CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_FOWNER
NoNewPrivileges=yes
# ── Filesystem ───────────────────────────────────────────────────────
#
# ProtectSystem=strict would be the obvious choice and is WRONG here: it
# remounts everything read-only, and quarantine has to remove a threat
# from wherever it landed. ProtectHome is off for the same reason — most
# of what we quarantine is under /home.
ProtectSystem=false
ProtectHome=false
ReadWritePaths=/var/lib/hound /var/log/hound
StateDirectory=hound
LogsDirectory=hound
RuntimeDirectory=hound
PrivateTmp=no
# ── Everything else we can shut off ──────────────────────────────────
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@clock @cpu-emulation @debug @module @mount @obsolete @raw-io @reboot @swap
UMask=0077
# ── Resources ────────────────────────────────────────────────────────
# A scanner that eats the machine is its own denial of service.
MemoryMax=1G
TasksMax=64
LimitNOFILE=65536
OOMScoreAdjust=-500
[Install]
WantedBy=multi-user.target