#!/usr/bin/env bash # # Build, gate, sign and publish the Hound Linux threat pack. # # Unlike the definition feed (rebuilt nightly from OSV), the threat pack is # curated YARA and changes only when a human edits the rules, so this is run # by hand — or by CI on a change to crates/hound-defs/rules/hound-linux.yar. # # The builder refuses to sign a pack that does not compile or that matches a # system binary (the goodware gate), so a bad edit fails here, not in the # field. Publishing is atomic per file and the index is rewritten last. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" KEY="${HOUND_DEFS_KEY:-$HOME/agents/hound/.secrets/defs-signing.key}" DEST="${HOUND_DEFS_DIR:-/srv/houndav/defs}" SRC="${HOUND_RULES_SRC:-$ROOT/crates/hound-defs/rules/hound-linux.yar}" NAME="hound-linux" VERSION="${1:-$(date -u +%Y.%m.%d)}" CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)" log() { printf '%s %s\n' "$(date -u +%H:%M:%S)" "$*"; } [ -f "$KEY" ] || { echo "no signing key at $KEY" >&2; exit 1; } [ -f "$SRC" ] || { echo "no rules source at $SRC" >&2; exit 1; } [ -d "$DEST" ] || { echo "no destination directory $DEST" >&2; exit 1; } BUILDER="$ROOT/target/release/examples/build-rules-pack" if [ ! -x "$BUILDER" ]; then log "building the rules-pack builder" ( cd "$ROOT" && cargo build --release -p houndd --example build-rules-pack ) fi STAGE="$(mktemp -d "${TMPDIR:-/var/tmp}/hound-rules-stage.XXXXXX")" trap 'rm -rf "$STAGE"' EXIT PACK="$STAGE/${NAME}-${VERSION}.rpack" # This compiles, runs the goodware gate against this host's binaries, and # signs — or exits non-zero without writing anything. "$BUILDER" "$SRC" "$NAME" "$PACK" "$KEY" "$VERSION" "$CREATED" base="$(basename "$PACK")" cp "$PACK" "$DEST/.$base.tmp" chmod 644 "$DEST/.$base.tmp" mv -f "$DEST/.$base.tmp" "$DEST/$base" log "published $base" # Rebuild the index over everything on disk — definition packs and rules # packs both. Same logic as refresh-definitions.sh so the two agree. python3 - "$DEST" <<'PY' import hashlib, json, os, sys dest = sys.argv[1] def newest_by_family(suffix, strip): newest = {} for f in sorted(os.listdir(dest)): if f.endswith(suffix): newest[f.rsplit("-", 1)[0]] = f out = [] for _, f in sorted(newest.items()): p = os.path.join(dest, f) out.append({ "file": f, "sha256": hashlib.sha256(open(p, "rb").read()).hexdigest(), "size": os.path.getsize(p), "version": f.rsplit("-", 1)[1][:-strip], }) return out packs = newest_by_family(".pack", 5) rules = newest_by_family(".rpack", 6) tmp = os.path.join(dest, ".index.json.tmp") with open(tmp, "w") as fh: json.dump({"packs": packs, "rules": rules}, fh, indent=2) fh.write("\n") os.chmod(tmp, 0o644) os.replace(tmp, os.path.join(dest, "index.json")) print(f"index.json lists {len(packs)} definition pack(s) and {len(rules)} rules pack(s)") PY log "done"