A clean laptop reported 988 critical rootkit findings; this server
3786, PID 1 among them. Every one was false, and the cause was our own
systemd hardening.
ProtectProc=invisible hides processes the daemon does not own from its
view of /proc, while kill(pid, 0) keeps answering truthfully because it
is a syscall and not a filesystem lookup. The hidden-process check
compares exactly those two sources, so with that setting every process
on the machine looked concealed. Enumerating processes is this daemon's
job, so it needs the default view.
Removing the setting is not enough on its own — hidepid= on the /proc
mount produces the same blindness and we do not control that. So the
detector now recognises when it cannot see:
- PID 1 is the control. It always exists and nothing hides init; a
rootkit that did would break the machine it is living on. If PID 1
answers kill(1, 0) but is absent from the listing, we are blind and
say so as info rather than crying rootkit.
- A plausibility ceiling of 32. Hiding a handful of processes is the
entire point of a rootkit; hundreds means a broken observer. An
antivirus that reports a critical rootkit finding on every clean
machine teaches people to ignore the one time it is real.
Also in this change, from testing on a real desktop:
- Closing the window hides it to the tray instead of exiting, with a
one-time notification so it does not read as a crash. Quit lives
only in the tray menu and confirms first. The settings already had
close_to_tray and confirm_quit fields wired to nothing; they are
honoured now rather than hardcoded.
- The tray menu and Scan Home sent the literal string "~". A shell
would have expanded it, nothing here did, so the daemon was asked
to scan a directory of that name. It failed silently until the
per-peer readability check made it audible.
- Administrative actions elevate through polkit instead of telling
people to open a terminal. The app tries unprivileged first and
only on a privilege refusal runs `pkexec hound admin-rpc`, which
forwards one request as root. auth_admin_keep, because prompting on
every settings toggle trains people to authenticate without reading
the prompt. This grants what `sudo hound` already grants to people
who could already run sudo — a transport, not a new privilege.
- `hound settings exec-gate on|off` now exists. The install script,
the AppImage banner, the rpm spec, the AUR install file and
llms.txt all told users to run `hound settings set exec_gate true`.
There was no `set` subcommand and no way to enable the execution
gate from the CLI at all: the flagship paid feature was unreachable
and the first thing a new user was told to type returned an error.
A test now asserts every documented command parses.
- `settings show` displays the exec gate state, and no longer prints
its own header twice.
- The CLI help still described ClamAV, which has not been the engine
for some time. So did the socket permission error, which now
explains the `hound` group and the log-out-and-back-in it needs.
368 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
4.4 KiB
Bash
Executable file
136 lines
4.4 KiB
Bash
Executable file
#!/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 exec-gate on
|
|
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"
|