Added Husky precommit support and added pre-commit hook that automatically downloads and caches the appropriate binary for your platform (no Docker or manual installation required).

This commit is contained in:
Mick Grove 2026-01-30 08:33:59 -08:00
commit 45cab25615
6 changed files with 548 additions and 4 deletions

View file

@ -1,6 +1,14 @@
- id: kingfisher-auto
name: kingfisher (auto-install)
description: Automatically downloads and caches the Kingfisher binary, then scans staged changes. No manual installation required.
entry: scripts/kingfisher-pre-commit-auto.sh
language: script
pass_filenames: false
stages: [commit]
- id: kingfisher-docker
name: kingfisher (docker)
description: Run Kingfisher in Docker against staged changes at the repository root. No local install required.
description: Run Kingfisher in Docker against staged changes at the repository root. Requires Docker but no local install.
entry: ghcr.io/mongodb/kingfisher:latest
language: docker
args: ["scan", ".", "--staged", "--quiet", "--no-update-check"]

View file

@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file.
- Added `kingfisher validate` subcommand to validate credentials without running a full scan.
- Refactored project into multiple crates for better modularity and maintainability.
- Ensured more CLI arguments are global and available across all subcommands.
- Added `kingfisher-auto` pre-commit hook that automatically downloads and caches the appropriate binary for your platform (no Docker or manual installation required).
- Added Husky integration support with `install-husky.sh` helper script and documentation for Node.js projects.
- Added `kingfisher-pre-commit-auto.sh` and `kingfisher-pre-commit-auto.ps1` scripts for automatic binary download in Git hooks (Linux, macOS, Windows support).
## [v1.76.0]
- Fixed validation deduplication for rules with nested unnamed captures (e.g. `(?<REGEX>...(ABC|DEF)...)`) to use the primary capture for grouping, ensuring each unique match triggers a separate validation request.

112
README.md
View file

@ -101,6 +101,7 @@ kingfisher scan /path/to/scan --access-map --view-report
- [macOS and Linux](#macos-and-linux)
- [Windows PowerShell](#windows-powershell)
- [Using the `pre-commit` framework](#using-the-pre-commit-framework)
- [Using Husky (Node.js projects)](#using-husky-nodejs-projects)
- [Compile](#compile)
- [ Run Kingfisher in Docker](#-run-kingfisher-in-docker)
- [🔐 Detection Rules at a Glance](#-detection-rules-at-a-glance)
@ -365,13 +366,32 @@ repos:
- repo: https://github.com/mongodb/kingfisher
rev: <version-or-commit>
hooks:
# No local install required; runs Kingfisher from Docker at the repo root
# Recommended: Auto-downloads and caches the binary - no manual install or Docker required
- id: kingfisher-auto
# Alternative: Runs Kingfisher from Docker (requires Docker)
- id: kingfisher-docker
# Fastest when you already have Kingfisher installed locally
# Alternative: Uses locally installed Kingfisher (fastest, requires manual install)
- id: kingfisher
```
**Available hooks:**
| Hook ID | Description | Requirements |
|---------|-------------|--------------|
| `kingfisher-auto` | Automatically downloads and caches the appropriate binary for your platform | curl, tar (or unzip on Windows) |
| `kingfisher-docker` | Runs Kingfisher in Docker | Docker |
| `kingfisher` | Uses locally installed Kingfisher binary | Manual installation |
The `kingfisher-auto` hook is recommended for most users as it:
- Automatically downloads the correct binary for your OS and architecture
- Caches the binary in `~/.cache/kingfisher` (Linux/macOS) or `%LOCALAPPDATA%\kingfisher` (Windows)
- Works across Linux, macOS, and Windows (via Git Bash which comes with Git for Windows)
- Requires no Docker or manual installation
**Windows users:** The `kingfisher-auto` hook uses a bash script that runs via Git Bash (included with [Git for Windows](https://gitforwindows.org/)). For native PowerShell, a `kingfisher-pre-commit-auto.ps1` script is also available in the `scripts/` directory.
Then install the hook via `pre-commit install`. Every hook now drives Kingfisher
directly with the built-in `--staged` flag:
@ -392,7 +412,93 @@ scans only those staged changes.
To trigger a hook in CI without installing to `.git/hooks`, run (for example):
```bash
pre-commit run kingfisher-pre-commit --all-files
pre-commit run kingfisher-auto --all-files
```
**Pin to a specific version:**
To use a specific Kingfisher version with the `kingfisher-auto` hook, set the `KINGFISHER_VERSION` environment variable:
```yaml
repos:
- repo: https://github.com/mongodb/kingfisher
rev: v1.76.0
hooks:
- id: kingfisher-auto
# Optional: pin to a specific kingfisher binary version
# env:
# KINGFISHER_VERSION: "1.76.0"
```
</details>
#### Using Husky (Node.js projects)
For Node.js projects using [Husky](https://typicode.github.io/husky/), you can add Kingfisher to your pre-commit hooks:
<details>
**Quick setup (recommended):**
```bash
# Initialize Husky if you haven't already
npx husky init
# Add Kingfisher to the pre-commit hook (auto-downloads binary)
echo 'curl -fsSL https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/kingfisher-pre-commit-auto.sh | bash' >> .husky/pre-commit
```
**Or use the helper script:**
```bash
curl -fsSL https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-husky.sh | bash -s -- --auto-install
```
**Available options:**
```bash
# Use auto-download (recommended - no pre-installation needed)
./scripts/install-husky.sh --auto-install
# Use Docker (requires Docker, no binary installation)
./scripts/install-husky.sh --use-docker
# Use local binary (requires kingfisher to be installed)
./scripts/install-husky.sh
# Uninstall
./scripts/install-husky.sh --uninstall
```
**Manual setup:**
If you prefer to configure Husky manually, add one of these to your `.husky/pre-commit`:
```bash
# Option 1: Auto-download binary (recommended)
curl -fsSL https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/kingfisher-pre-commit-auto.sh | bash
# Option 2: Use Docker
docker run --rm -v "$(pwd)":/src ghcr.io/mongodb/kingfisher:latest scan /src --staged --quiet --no-update-check
# Option 3: Use locally installed binary
kingfisher scan . --staged --quiet --no-update-check
```
**Windows with PowerShell:**
For Windows users preferring native PowerShell over Git Bash, create a `.husky/pre-commit.ps1` or add to your hook:
```powershell
# Download and run the PowerShell auto-install script
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/kingfisher-pre-commit-auto.ps1' -OutFile "$env:TEMP\kf-scan.ps1"
& "$env:TEMP\kf-scan.ps1"
```
Or if Kingfisher is already installed:
```powershell
kingfisher scan . --staged --quiet --no-update-check
```
</details>

148
scripts/install-husky.sh Executable file
View file

@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Install Kingfisher as a Husky pre-commit hook
# Usage: ./install-husky.sh [--uninstall]
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: install-husky.sh [OPTIONS]
Adds Kingfisher to your Husky pre-commit hook.
Options:
--uninstall Remove Kingfisher from the Husky pre-commit hook
--use-docker Use Docker instead of local binary (no installation needed)
--auto-install Auto-download kingfisher binary if not present
-h, --help Show this help message
Requirements:
- Node.js project with Husky already initialized
- OR run this script after 'npx husky init'
USAGE
}
UNINSTALL=false
USE_DOCKER=false
AUTO_INSTALL=false
while [[ $# -gt 0 ]]; do
case "$1" in
--uninstall)
UNINSTALL=true
shift
;;
--use-docker)
USE_DOCKER=true
shift
;;
--auto-install)
AUTO_INSTALL=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done
# Find Husky directory
find_husky_dir() {
if [[ -d ".husky" ]]; then
echo ".husky"
elif [[ -d ".config/husky" ]]; then
echo ".config/husky"
else
echo ""
fi
}
HUSKY_DIR="$(find_husky_dir)"
if [[ -z "$HUSKY_DIR" ]]; then
echo "Error: Husky directory not found." >&2
echo "Initialize Husky first with: npx husky init" >&2
exit 1
fi
PRE_COMMIT="$HUSKY_DIR/pre-commit"
MARKER="# kingfisher-scan"
# Determine the scan command
if $USE_DOCKER; then
SCAN_CMD='docker run --rm -v "$(pwd)":/src ghcr.io/mongodb/kingfisher:latest scan /src --staged --quiet --no-update-check'
elif $AUTO_INSTALL; then
# Use the auto-download script approach
SCAN_CMD='curl -fsSL https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/kingfisher-pre-commit-auto.sh | bash'
else
SCAN_CMD='kingfisher scan . --staged --quiet --no-update-check'
fi
uninstall() {
if [[ -f "$PRE_COMMIT" ]]; then
# Remove kingfisher lines from pre-commit
if grep -q "$MARKER" "$PRE_COMMIT"; then
# Create temp file without kingfisher lines
local tmpfile
tmpfile="$(mktemp)"
grep -v "$MARKER" "$PRE_COMMIT" | grep -v "kingfisher scan\|kingfisher:latest\|kingfisher-pre-commit-auto" > "$tmpfile" || true
mv "$tmpfile" "$PRE_COMMIT"
chmod +x "$PRE_COMMIT"
echo "Kingfisher removed from $PRE_COMMIT"
else
echo "Kingfisher not found in $PRE_COMMIT"
fi
else
echo "No pre-commit hook found at $PRE_COMMIT"
fi
}
install() {
# Create pre-commit if it doesn't exist
if [[ ! -f "$PRE_COMMIT" ]]; then
cat > "$PRE_COMMIT" <<'EOF'
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
EOF
chmod +x "$PRE_COMMIT"
echo "Created $PRE_COMMIT"
fi
# Check if kingfisher is already installed
if grep -q "$MARKER" "$PRE_COMMIT" 2>/dev/null; then
echo "Kingfisher is already configured in $PRE_COMMIT"
return 0
fi
# Append kingfisher scan command
cat >> "$PRE_COMMIT" <<EOF
$MARKER
$SCAN_CMD
EOF
echo "Kingfisher added to $PRE_COMMIT"
if ! $USE_DOCKER && ! $AUTO_INSTALL; then
if ! command -v kingfisher >/dev/null 2>&1; then
echo ""
echo "Note: kingfisher is not installed. Install it with:"
echo " curl -fsSL https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-kingfisher.sh | bash"
echo ""
echo "Or re-run this script with --use-docker or --auto-install"
fi
fi
}
if $UNINSTALL; then
uninstall
else
install
fi

View file

@ -0,0 +1,143 @@
<#
.SYNOPSIS
Kingfisher pre-commit hook with automatic binary download for Windows.
.DESCRIPTION
Downloads and caches the Kingfisher binary, then scans staged changes.
No manual installation required.
.PARAMETER Version
Specific version to download (e.g., "1.76.0" or "v1.76.0").
Defaults to "latest".
.EXAMPLE
./kingfisher-pre-commit-auto.ps1
.EXAMPLE
$env:KINGFISHER_VERSION = "1.76.0"; ./kingfisher-pre-commit-auto.ps1
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$repo = 'mongodb/kingfisher'
$cacheDir = if ($env:KINGFISHER_CACHE_DIR) {
$env:KINGFISHER_CACHE_DIR
} else {
Join-Path $env:LOCALAPPDATA 'kingfisher'
}
$kingfisherBin = Join-Path $cacheDir 'kingfisher.exe'
$versionFile = Join-Path $cacheDir '.version'
$expectedVersion = if ($env:KINGFISHER_VERSION) { $env:KINGFISHER_VERSION } else { 'latest' }
function Get-AssetName {
# Windows only supports x64 currently
return 'kingfisher-windows-x64.zip'
}
function Download-Kingfisher {
param(
[string]$Version
)
$assetName = Get-AssetName
if (-not (Test-Path $cacheDir)) {
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
}
$tempDir = New-Item -ItemType Directory -Path ([System.IO.Path]::GetTempPath()) -Name ([System.Guid]::NewGuid().ToString())
try {
if ($Version -eq 'latest') {
$downloadUrl = "https://github.com/$repo/releases/latest/download/$assetName"
Write-Host "Downloading kingfisher (latest) for Windows..." -ForegroundColor Cyan
} else {
# Support both "v1.76.0" and "1.76.0" formats
if (-not $Version.StartsWith('v')) {
$Version = "v$Version"
}
$downloadUrl = "https://github.com/$repo/releases/download/$Version/$assetName"
Write-Host "Downloading kingfisher ($Version) for Windows..." -ForegroundColor Cyan
}
$archivePath = Join-Path $tempDir.FullName $assetName
try {
Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath -UseBasicParsing
} catch {
Write-Error "Failed to download $downloadUrl : $_"
exit 1
}
Write-Host "Extracting archive..." -ForegroundColor Cyan
Expand-Archive -Path $archivePath -DestinationPath $tempDir.FullName -Force
$extractedBinary = Join-Path $tempDir.FullName 'kingfisher.exe'
if (-not (Test-Path $extractedBinary)) {
Write-Error "Binary not found in downloaded archive"
exit 1
}
Copy-Item -Path $extractedBinary -Destination $kingfisherBin -Force
# Store the version we downloaded
if ($Version -eq 'latest') {
try {
$versionOutput = & $kingfisherBin --version 2>$null | Select-Object -First 1
Set-Content -Path $versionFile -Value $versionOutput -NoNewline
} catch {
Set-Content -Path $versionFile -Value 'latest' -NoNewline
}
} else {
Set-Content -Path $versionFile -Value $Version -NoNewline
}
Write-Host "Kingfisher installed to $kingfisherBin" -ForegroundColor Green
}
finally {
if ($tempDir -and (Test-Path $tempDir.FullName)) {
Remove-Item -Path $tempDir.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
function Test-NeedsDownload {
# Binary doesn't exist
if (-not (Test-Path $kingfisherBin)) {
return $true
}
# No version tracking - always use existing binary for 'latest'
if ($expectedVersion -eq 'latest') {
return $false
}
# Check if version matches
if (Test-Path $versionFile) {
$installedVersion = Get-Content -Path $versionFile -Raw
# Normalize version format for comparison
$expectedNormalized = $expectedVersion
if (-not $expectedNormalized.StartsWith('v')) {
$expectedNormalized = "v$expectedNormalized"
}
if ($installedVersion -like "*$expectedNormalized*" -or $installedVersion -eq $expectedVersion) {
return $false
}
}
return $true
}
# Main execution
if (Test-NeedsDownload) {
Download-Kingfisher -Version $expectedVersion
}
# Run kingfisher scan on staged changes
# Pass through any additional arguments
& $kingfisherBin scan . --staged --quiet --no-update-check @args
exit $LASTEXITCODE

View file

@ -0,0 +1,136 @@
#!/usr/bin/env bash
# Kingfisher pre-commit hook with automatic binary download
# This script downloads the appropriate kingfisher binary if not already cached,
# then runs the scan against staged changes.
set -euo pipefail
REPO="mongodb/kingfisher"
CACHE_DIR="${KINGFISHER_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/kingfisher}"
KINGFISHER_BIN="$CACHE_DIR/kingfisher"
VERSION_FILE="$CACHE_DIR/.version"
# Determine the expected version from the pre-commit rev (passed as env var or default to latest)
EXPECTED_VERSION="${KINGFISHER_VERSION:-latest}"
get_platform() {
local os arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Linux) platform="linux" ;;
Darwin) platform="darwin" ;;
MINGW*|MSYS*|CYGWIN*) platform="windows" ;;
*) echo "Error: Unsupported OS '$os'" >&2; exit 1 ;;
esac
case "$arch" in
x86_64|amd64) arch_suffix="x64" ;;
arm64|aarch64) arch_suffix="arm64" ;;
*) echo "Error: Unsupported architecture '$arch'" >&2; exit 1 ;;
esac
echo "${platform}-${arch_suffix}"
}
download_kingfisher() {
local platform="$1"
local version="$2"
local ext="tgz"
if [[ "$platform" == windows-* ]]; then
ext="zip"
fi
local asset_name="kingfisher-${platform}.${ext}"
local download_url
if [[ "$version" == "latest" ]]; then
download_url="https://github.com/${REPO}/releases/latest/download/${asset_name}"
else
# Support both "v1.76.0" and "1.76.0" formats
if [[ "$version" != v* ]]; then
version="v${version}"
fi
download_url="https://github.com/${REPO}/releases/download/${version}/${asset_name}"
fi
mkdir -p "$CACHE_DIR"
local tmpdir
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
echo "Downloading kingfisher ($version) for $platform..." >&2
if ! curl -fLsS "$download_url" -o "$tmpdir/$asset_name"; then
echo "Error: Failed to download $download_url" >&2
exit 1
fi
if [[ "$ext" == "zip" ]]; then
unzip -q "$tmpdir/$asset_name" -d "$tmpdir"
local binary_name="kingfisher.exe"
else
tar -C "$tmpdir" -xzf "$tmpdir/$asset_name"
local binary_name="kingfisher"
fi
if [[ ! -f "$tmpdir/$binary_name" ]]; then
echo "Error: Binary not found in downloaded archive" >&2
exit 1
fi
mv "$tmpdir/$binary_name" "$KINGFISHER_BIN"
chmod +x "$KINGFISHER_BIN"
# Store the version we downloaded
if [[ "$version" == "latest" ]]; then
"$KINGFISHER_BIN" --version 2>/dev/null | head -1 > "$VERSION_FILE" || echo "latest" > "$VERSION_FILE"
else
echo "$version" > "$VERSION_FILE"
fi
echo "Kingfisher installed to $KINGFISHER_BIN" >&2
}
needs_download() {
# Binary doesn't exist
if [[ ! -x "$KINGFISHER_BIN" ]]; then
return 0
fi
# No version tracking - always use existing binary for 'latest'
if [[ "$EXPECTED_VERSION" == "latest" ]]; then
return 1
fi
# Check if version matches
if [[ -f "$VERSION_FILE" ]]; then
local installed_version
installed_version="$(cat "$VERSION_FILE")"
# Normalize version format for comparison
local expected_normalized="$EXPECTED_VERSION"
if [[ "$expected_normalized" != v* ]]; then
expected_normalized="v${expected_normalized}"
fi
if [[ "$installed_version" == *"$expected_normalized"* ]] || [[ "$installed_version" == "$EXPECTED_VERSION" ]]; then
return 1
fi
fi
return 0
}
main() {
local platform
platform="$(get_platform)"
if needs_download; then
download_kingfisher "$platform" "$EXPECTED_VERSION"
fi
# Run kingfisher scan on staged changes
exec "$KINGFISHER_BIN" scan . --staged --quiet --no-update-check "$@"
}
main "$@"