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>
79 lines
2.9 KiB
Bash
Executable file
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"
|