#!/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 — every definition pack # (baselines and deltas) and every rules pack. Identical logic to # refresh-definitions.sh so running either keeps the delta feed intact; # version comes from inside the signed payload, not the filename. python3 - "$DEST" <<'PY' import base64, hashlib, json, os, sys dest = sys.argv[1] def entry(f): raw = open(os.path.join(dest, f), "rb").read() try: version = json.loads(base64.b64decode(json.loads(raw)["payload"])).get("version", "") except Exception: version = "" return {"file": f, "sha256": hashlib.sha256(raw).hexdigest(), "size": len(raw), "version": version} packs, community, rules = [], [], [] for f in sorted(os.listdir(dest)): if not os.path.isfile(os.path.join(dest, f)): continue if f.endswith(".rpack"): rules.append(entry(f)) elif f.startswith("community-") and f.endswith(".pack"): community.append(entry(f)) elif f.endswith(".pack"): packs.append(entry(f)) tmp = os.path.join(dest, ".index.json.tmp") with open(tmp, "w") as fh: json.dump({"packs": packs, "community": community, "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: {len(packs)} feed, {len(community)} community, {len(rules)} rules pack(s)") PY log "done"