Antivirus/tools/publish-rules-pack.sh
dev 79ea89713e Phase 1: license enforcement, threat pack, Apache-2.0, real Action verification
Make the product buyable and the open-source claim true.

Licence system, end to end. license.rs was well-designed dead code; wire
it up: an Ed25519-signed token (same key and verify-before-parse discipline
as definition packs), `hound license install`, houndd loads and verifies at
boot, and the execution gate and full supply-chain feed now gate on
Capability checks. Verification failing always degrades to Free, never to a
locked-out security tool; an expired licence downgrades with the reason
shown. Adds tools/issue-license.py.

Hound Linux threat pack. 34 curated YARA rules — miners, IoT/DDoS bots,
backdoors, rootkits, ransomware, webshells, droppers, reverse shells —
shipped through a new signed rules-pack channel (.rpack) alongside the
definitions feed. Every rule is ELF- or size-anchored and keyed on
family strings, never syscalls; the builder refuses to sign a pack that
matches a system binary (the goodware gate caught two bad rules), and a
regression test proves every rule fires on a sample and stays quiet on a
document about malware.

Action signature verification. The composite action claimed Ed25519
verification "against the same signed manifest the desktop agent uses" but
only compared a same-host sha256. It now fetches latest.json, verifies the
Ed25519 signature over the canonical release statement against the pinned
release key, and installs the checksum from the verified manifest.

Licence resolved to Apache-2.0: Cargo.toml, a real LICENSE file, README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 15:51:33 -05:00

79 lines
2.9 KiB
Bash
Executable file

#!/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"