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>
132 lines
5.4 KiB
YAML
132 lines
5.4 KiB
YAML
name: 'Hound Security Scan'
|
|
description: 'Find exposed credentials, malicious dependencies and unsafe CI in a repository'
|
|
author: 'Hound Antivirus'
|
|
branding:
|
|
icon: 'shield'
|
|
color: 'purple'
|
|
|
|
inputs:
|
|
path:
|
|
description: 'Directory to scan, relative to the repository root'
|
|
required: false
|
|
default: '.'
|
|
history:
|
|
description: 'Also walk git history for credentials that were removed but not revoked'
|
|
required: false
|
|
default: 'false'
|
|
fail-on:
|
|
description: 'Fail the job at this severity or above: critical | warning | never'
|
|
required: false
|
|
default: 'critical'
|
|
annotate:
|
|
description: 'Annotate the affected files in the diff view'
|
|
required: false
|
|
default: 'true'
|
|
version:
|
|
description: 'Hound version to use, or "latest"'
|
|
required: false
|
|
default: 'latest'
|
|
|
|
outputs:
|
|
critical:
|
|
description: 'Number of critical findings'
|
|
value: ${{ steps.scan.outputs.critical }}
|
|
warnings:
|
|
description: 'Number of warnings'
|
|
value: ${{ steps.scan.outputs.warnings }}
|
|
report:
|
|
description: 'Path to the JSON report'
|
|
value: ${{ steps.scan.outputs.report }}
|
|
|
|
runs:
|
|
using: 'composite'
|
|
steps:
|
|
- id: install
|
|
shell: bash
|
|
# The release manifest is Ed25519-signed with Hound's release key —
|
|
# the same key and canonical statement the desktop agent verifies
|
|
# before it self-updates. The public key is pinned below, and the
|
|
# checksum used for the download comes out of the verified statement,
|
|
# so neither the download host nor the manifest host can substitute a
|
|
# different binary.
|
|
run: |
|
|
set -euo pipefail
|
|
curl -fsSL https://dl.houndav.com/latest.json -o /tmp/hound-latest.json
|
|
python3 - /tmp/hound-latest.json <<'PY'
|
|
import json, sys
|
|
m = json.load(open(sys.argv[1]))
|
|
if m.get("key_id") != "hound-2026":
|
|
sys.exit(f"::error::release manifest signed by unexpected key {m.get('key_id')!r}")
|
|
r = m["release"]
|
|
# Must match release.rs::canonical / publish-release.py byte for byte.
|
|
canonical = ("hound-release-v1\n"
|
|
f"version={r['version']}\n"
|
|
f"notes_url={r['notes_url']}\n"
|
|
f"deb_url={r['deb_url']}\n"
|
|
f"deb_sha256={r['deb_sha256']}\n"
|
|
f"published={r['published']}\n")
|
|
open("/tmp/hound-canonical", "wb").write(canonical.encode())
|
|
open("/tmp/hound-sig", "wb").write(bytes.fromhex(m["signature"]))
|
|
# Hound's release public key (id hound-2026), DER-wrapped for openssl.
|
|
pub = bytes.fromhex("302a300506032b6570032100"
|
|
"12ba519f13e6e83700ef3efb07e93285"
|
|
"c48879302604a320a02dc3642990b451")
|
|
open("/tmp/hound-pub.der", "wb").write(pub)
|
|
with open("/tmp/hound-release.env", "w") as f:
|
|
f.write(f"version={r['version']}\n")
|
|
f.write(f"deb_url={r['deb_url']}\n")
|
|
f.write(f"deb_sha256={r['deb_sha256']}\n")
|
|
PY
|
|
openssl pkey -pubin -inform DER -in /tmp/hound-pub.der -out /tmp/hound-pub.pem
|
|
if ! openssl pkeyutl -verify -pubin -inkey /tmp/hound-pub.pem -rawin \
|
|
-in /tmp/hound-canonical -sigfile /tmp/hound-sig >/dev/null; then
|
|
echo "::error::the Hound release manifest failed Ed25519 verification — refusing to install"
|
|
exit 1
|
|
fi
|
|
source /tmp/hound-release.env
|
|
want='${{ inputs.version }}'
|
|
if [ "$want" = latest ] || [ "$want" = "$version" ]; then
|
|
want="$version"
|
|
url="$deb_url"
|
|
expected="$deb_sha256"
|
|
else
|
|
# A pinned older version is outside the signed manifest. Its
|
|
# published checksum still has to match, which catches corruption
|
|
# but not a hostile host — pin `latest` (the default) for the
|
|
# full signature guarantee.
|
|
echo "::warning::pinned version ${want} predates the signed manifest; install is checksum-verified only"
|
|
url="https://dl.houndav.com/deb/hound_${want}_amd64.deb"
|
|
expected="$(curl -fsSL "${url}.sha256" | cut -d' ' -f1)"
|
|
fi
|
|
curl -fsSL "$url" -o /tmp/hound.deb
|
|
actual="$(sha256sum /tmp/hound.deb | cut -d' ' -f1)"
|
|
if [ "$expected" != "$actual" ]; then
|
|
echo "::error::the Hound download does not match its verified checksum"
|
|
exit 1
|
|
fi
|
|
sudo apt-get install -y -qq /tmp/hound.deb >/dev/null
|
|
echo "installed hound $want (release manifest signature verified)"
|
|
|
|
- id: scan
|
|
shell: bash
|
|
working-directory: ${{ github.workspace }}
|
|
run: |
|
|
set -uo pipefail
|
|
args=""
|
|
if [ '${{ inputs.history }}' = 'true' ]; then args="--history"; fi
|
|
|
|
# Two reports: the supply-chain sweep and the hygiene checks. They
|
|
# share a finding shape, so the outputs merge cleanly.
|
|
hound hygiene '${{ inputs.path }}' $args --json > /tmp/hygiene.json || true
|
|
hound supply-chain '${{ inputs.path }}' --json > /tmp/supply.json 2>/dev/null || true
|
|
|
|
python3 "$GITHUB_ACTION_PATH/report.py" \
|
|
--hygiene /tmp/hygiene.json \
|
|
--supply /tmp/supply.json \
|
|
--annotate '${{ inputs.annotate }}' \
|
|
--fail-on '${{ inputs.fail-on }}' \
|
|
--summary "${GITHUB_STEP_SUMMARY:-/dev/null}" \
|
|
--out /tmp/hound-report.json
|
|
status=$?
|
|
echo "report=/tmp/hound-report.json" >> "$GITHUB_OUTPUT"
|
|
exit $status
|