Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,38 @@ env:
RUST_BACKTRACE: short

jobs:
distribution:
name: distribution
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Validate GitHub Action and release automation
run: python3 scripts/action_validation/validate.py

- name: Build container
run: docker build --tag keywatch:ci .

- name: Smoke-test container
shell: bash
run: |
set -euo pipefail
docker run --rm keywatch:ci --version
test "$(docker run --rm --entrypoint id keywatch:ci -u)" != "0"

set +e
printf '%s\n' 'AWS_KEY=AKIAIOSFODNN7EXAMPLE' | \
docker run --rm -i keywatch:ci scan --stdin --exit-mode strict
scan_status=$?
set -e
if [ "$scan_status" -ne 1 ]; then
echo "ERROR: container secret scan exited with $scan_status instead of 1" >&2
exit 1
fi

formatting:
name: cargo-fmt
runs-on: ubuntu-latest
Expand Down
32 changes: 24 additions & 8 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@ name: Publish Docker Image
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: 'Image tag'
required: true
default: 'latest'

permissions:
contents: read
packages: write
attestations: write
id-token: write

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
IMAGE_NAME: pixincreate/keywatch

jobs:
build-and-push:
Expand All @@ -27,6 +23,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v7

- name: Verify tag and package versions
shell: bash
run: |
set -euo pipefail
tag_version=${GITHUB_REF_NAME#v}
cargo_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n 1)
action_version=$(sed -n "/^ version:$/,/^ paths:$/ s/^ default: '\([^']*\)'/\1/p" action.yml)
test "$tag_version" = "$cargo_version"
test "$tag_version" = "$action_version"
grep -Fq "## [$tag_version] -" CHANGELOG.md

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4.6.0
with:
Expand All @@ -42,13 +49,22 @@ jobs:
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ github.event_name == 'push' && !contains(github.ref_name, '-') }}
type=sha,format=short
type=raw,value=${{ inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}

- name: Build and push Docker image
id: push
uses: docker/build-push-action@v7.3.0
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Attest image provenance
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
54 changes: 52 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,62 @@ on:
tags: ["v*"]

permissions:
contents: write
contents: read

jobs:
preflight:
name: release-preflight
runs-on: ubuntu-latest
timeout-minutes: 30
env:
RUSTFLAGS: "-D warnings"

steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- uses: Swatinem/rust-cache@v2.9.1

- name: Verify tag and package versions
shell: bash
run: |
set -euo pipefail
tag_version=${GITHUB_REF_NAME#v}
cargo_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n 1)
action_version=$(sed -n "/^ version:$/,/^ paths:$/ s/^ default: '\([^']*\)'/\1/p" action.yml)
test "$tag_version" = "$cargo_version"
test "$tag_version" = "$action_version"
grep -Fq "## [$tag_version] -" CHANGELOG.md

- name: Validate distribution automation
run: python3 scripts/action_validation/validate.py

- name: Check formatting
run: cargo fmt --all --check

- name: Run tests
run: cargo test --all-features --all-targets

- name: Run tests (release)
run: cargo test --release --all-features --all-targets

- name: Run Clippy
run: cargo clippy --all-features --all-targets -- -D warnings

- name: Check package
run: cargo package --locked

release:
name: release-${{ matrix.platform.asset_name }}
needs: preflight
runs-on: ${{ matrix.platform.os }}
permissions:
contents: write
strategy:
matrix:
platform:
Expand Down Expand Up @@ -71,7 +121,7 @@ jobs:
# Build the specific package
- name: Build Binary
run: |
cargo build --release --target ${{ matrix.platform.target }}
cargo build --locked --release --target ${{ matrix.platform.target }}

- name: Prepare Asset
shell: bash
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ All notable changes to this project will be documented in this file.
- Cloud/monitoring/AI service detectors: Vercel, Netlify, Supabase, Datadog, New Relic, Sentry, PagerDuty, Anthropic, HuggingFace, Groq, Replicate, LangSmith
- **GitHub Action** — composite action (`action.yml`) for CI/CD integration
- **Docker support** — multi-stage Dockerfile with `--locked` flag, stripped binary, non-root user, and git installed for `--git-history` scanning and hook installation
- **Public distribution verification** — the root GitHub Action verifies release binary and detector checksums, while GHCR images publish semver, major, and latest tags with provenance
- `.dockerignore` for optimized Docker builds
- **Config file support** — `.keywatch.toml` with custom rules, detector overrides, and exclude patterns
- **SARIF 2.1.0 output** — `--format sarif` enables GitHub Code Scanning and SARIF viewer integration
Expand All @@ -39,6 +40,8 @@ All notable changes to this project will be documented in this file.
- Local hook installation now resolves Git's hooks directory directly, improving worktree and submodule compatibility
- `exit-mode critical` now fails on both HIGH and CRITICAL findings
- Detector descriptions and comments cleaned up for minimal noise
- Release preparation now synchronizes the Action version with Cargo metadata and runs CI before publishing tags
- CI now validates Action shell behavior, checksum failures, release automation, and container smoke behavior

### Fixed

Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,60 @@ Requires Rust 1.85+ (edition 2024) when building from source.
The canonical command is `key-watch`.
`keywatch` and `kw` are optional shell aliases exposed via `key-watch init ...`.

## GitHub Action

Use the root Action from a public workflow. The major tag follows compatible `2.x` releases; pin an exact release tag or commit SHA when immutable dependencies are required.

```yaml
name: Secret scan

on:
pull_request:
push:

permissions:
contents: read

jobs:
keywatch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- id: keywatch
uses: pixincreate/KeyWatch@v2
with:
paths: "."
exit-mode: strict
```

The Action installs the synchronized KeyWatch release, verifies SHA-256 checksums for the binary and `detectors.toml`, disables repository detector discovery, and writes a JSON report. It supports Linux x64 and macOS x64/arm64 runners; Windows runners are not supported.

| Input | Default | Purpose |
| ----------- | ---------------------- | ------------------------------------------------------------------------ |
| `version` | Action release version | Exact KeyWatch release to install |
| `paths` | `.` | Space-separated paths or globs to scan |
| `args` | empty | Additional scanner arguments that do not override Action-managed options |
| `exit-mode` | `strict` | `strict`, `critical`, or `always` |
| `output` | temporary report | JSON report path |
| `config` | empty | Explicit trusted `.keywatch.toml` path |
| `verbose` | `false` | Deprecated; enabling it is rejected to prevent secret disclosure in logs |

The `findings-count` and `exit-code` outputs are available as `${{ steps.keywatch.outputs['findings-count'] }}` and `${{ steps.keywatch.outputs['exit-code'] }}`.

## Container Image

The GitHub Container Registry image is a separate distribution channel for Linux x64 environments:

```sh
docker pull ghcr.io/pixincreate/keywatch:2
docker run --rm \
--volume "$PWD:/workspace:ro" \
--workdir /workspace \
ghcr.io/pixincreate/keywatch:2 scan .
```

Images are published as `x.y.z`, `x.y`, `x`, and `latest`, with build provenance attached. Exact semver tags are the reproducible choice. After the first publication, a repository owner must make the GHCR package public in the package settings to allow anonymous pulls; no separate GHCR account is required. The image runs as a non-root user and uses the image-owned detector configuration at `/etc/keywatch/detectors.toml`.

## Uninstall

### If installed with `cargo install`
Expand Down
54 changes: 40 additions & 14 deletions .github/actions/keywatch-scan/action.yml → action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ description: 'Scan files and directories for secrets with KeyWatch'
author: 'Pa1Nark'

inputs:
version:
description: 'Exact KeyWatch release version to install'
required: false
default: '1.1.0'
paths:
description: 'Paths to scan (space-separated, supports globs)'
required: false
Expand Down Expand Up @@ -43,34 +47,31 @@ runs:
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail

for required_tool in curl jq; do
for required_tool in curl; do
if ! command -v "$required_tool" >/dev/null 2>&1; then
echo "ERROR: $required_tool is required on this runner" >&2
exit 1
fi
done

REPO="pixincreate/KeyWatch"
VERSION="${KEYWATCH_VERSION:-latest}"
VERSION="$INPUT_VERSION"

curl_args=(-fsSL)
if [ -n "${GITHUB_TOKEN:-}" ]; then
curl_args+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi

if [ "$VERSION" = "latest" ]; then
release_json=$(curl "${curl_args[@]}" \
"https://api.github.com/repos/$REPO/releases/latest")
VERSION=$(jq -er '.tag_name' <<<"$release_json")
elif [[ "$VERSION" != v* ]]; then
if [[ "$VERSION" != v* ]]; then
VERSION="v$VERSION"
fi

if [[ ! "$VERSION" =~ ^v[0-9A-Za-z._-]+$ ]]; then
echo "ERROR: invalid KeyWatch version/tag '$VERSION'" >&2
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "ERROR: invalid KeyWatch release version '$VERSION'" >&2
exit 1
fi

Expand Down Expand Up @@ -109,16 +110,41 @@ runs:
binary_path="$bin_dir/key-watch$exe_suffix"
asset_name="keywatch-$asset_os-$asset_arch$exe_suffix"
binary_url="https://github.com/$REPO/releases/download/$VERSION/$asset_name"
config_url="https://raw.githubusercontent.com/$REPO/$VERSION/detectors.toml"
binary_checksum_url="$binary_url.sha256"
config_url="https://github.com/$REPO/releases/download/$VERSION/detectors.toml"
config_checksum_url="$config_url.sha256"
binary_checksum_path="$install_dir/$asset_name.sha256"
config_checksum_path="$install_dir/detectors.toml.sha256"

mkdir -p "$bin_dir"

echo "Downloading KeyWatch $VERSION for $asset_os-$asset_arch..."
curl -fsSL "$binary_url" -o "$binary_path"
chmod +x "$binary_path"
curl "${curl_args[@]}" "$binary_url" -o "$binary_path"
curl "${curl_args[@]}" "$binary_checksum_url" -o "$binary_checksum_path"

echo "Downloading KeyWatch detectors config from release $VERSION..."
curl "${curl_args[@]}" "$config_url" -o "$config_path"
curl "${curl_args[@]}" "$config_checksum_url" -o "$config_checksum_path"

expected_binary_checksum=$(awk 'NR == 1 { print $1 }' "$binary_checksum_path")
expected_config_checksum=$(awk 'NR == 1 { print $1 }' "$config_checksum_path")
if [ "$asset_os" = "darwin" ]; then
actual_binary_checksum=$(shasum -a 256 "$binary_path" | awk '{ print $1 }')
actual_config_checksum=$(shasum -a 256 "$config_path" | awk '{ print $1 }')
else
actual_binary_checksum=$(sha256sum "$binary_path" | awk '{ print $1 }')
actual_config_checksum=$(sha256sum "$config_path" | awk '{ print $1 }')
fi
if [ "$actual_binary_checksum" != "$expected_binary_checksum" ]; then
echo "ERROR: KeyWatch binary checksum verification failed" >&2
exit 1
fi
if [ "$actual_config_checksum" != "$expected_config_checksum" ]; then
echo "ERROR: detectors.toml checksum verification failed" >&2
exit 1
fi

echo "Downloading KeyWatch detectors config from $VERSION..."
curl -fsSL "$config_url" -o "$config_path"
chmod +x "$binary_path"

export PATH="$bin_dir:$PATH"
export KEYWATCH_CONFIG_PATH="$config_path"
Expand Down
Loading