Antivirus/packaging/build-appimage.sh
dev a3f31288fa rootkit: stop reporting every process on the machine as hidden
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>
2026-08-21 11:21:06 -05:00

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"