Antivirus/action/action.yml
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

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