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