diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 91f652c..8d62b2a 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -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"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 47de788..0bbdc75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. `(?...(ABC|DEF)...)`) to use the primary capture for grouping, ensuring each unique match triggers a separate validation request. diff --git a/README.md b/README.md index 31f6c9d..b996026 100644 --- a/README.md +++ b/README.md @@ -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: 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" +``` + + + +#### 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: + +
+ +**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 ```
diff --git a/scripts/install-husky.sh b/scripts/install-husky.sh new file mode 100755 index 0000000..53fb6e6 --- /dev/null +++ b/scripts/install-husky.sh @@ -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" </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 diff --git a/scripts/kingfisher-pre-commit-auto.ps1 b/scripts/kingfisher-pre-commit-auto.ps1 new file mode 100644 index 0000000..200bbb6 --- /dev/null +++ b/scripts/kingfisher-pre-commit-auto.ps1 @@ -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 diff --git a/scripts/kingfisher-pre-commit-auto.sh b/scripts/kingfisher-pre-commit-auto.sh new file mode 100755 index 0000000..e8d1ffb --- /dev/null +++ b/scripts/kingfisher-pre-commit-auto.sh @@ -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 "$@"