Skip to content

KeyWatch v3.0.0

Latest

Choose a tag to compare

@github-actions github-actions released this 14 Sep 18:00
· 2 commits to master since this release
v3.0.0
c46e3cb

Breaking changes

  • A nonexistent scan path, a symlink operand, or an operand that is not a regular file or directory is a hard error (exit 2) instead of a silent clean pass; scripts that relied on the old exit 0 must handle the error
  • Pre-push hooks scan the pushed revision ranges from the hook's stdin instead of the worktree; reinstall hooks with key-watch hook install pre-push to pick up the new script
  • scan --git-history walks every ref (git log --all) instead of only the checked-out branch; pass --rev-range to narrow the walk
  • Unknown keys in .keywatch.toml and detector files are rejected instead of silently ignored; fix any misspelled keys before upgrading (description on rules remains accepted)
  • Identity-number detectors (SSN, Aadhaar, PAN, Voter ID, ABHA) require the line to name the document; bare number dumps without a label on the same line are no longer flagged
  • Baseline migration: fingerprints now anchor to the repository root, so baselines written from a subdirectory by earlier releases stop suppressing, and entries from the renamed Stripe/Kimi detectors re-fire once; run key-watch scan . --update-baseline after upgrading to refresh the baseline

Added

  • scan --staged scans only the lines a commit adds
  • Baselines are auto-discovered from .keywatch-baseline.json; --no-baseline-discovery opts out
  • update-baseline workflow regenerates the baseline via a pull request
  • scan --fail-on-unscannable fails a strict scan when a file could not be read; the pre-commit hook passes it so an unscannable staged file cannot pass silently
  • CI scans this repository with KeyWatch and fails if the committed baseline has drifted
  • --prune-baseline rewrites the baseline from current findings, dropping entries for deleted files and rotated credentials; requires --update-baseline and a whole-tree scan, and prints what it dropped
  • scan --git-history --rev-range <range> restricts a history scan to a revision range; the pre-push hook uses it to scan exactly the pushed commits
  • Detectors for GitHub fine-grained PATs (github_pat_), GitHub refresh tokens (ghr_), AWS temporary keys (ASIA/ABIA/ACCA), AWS secret access keys (context-gated), modern Slack bot tokens, Slack app tokens (xapp-), Slack webhook URLs, and modern OpenAI project keys (sk-proj-/sk-svcacct-/sk-admin-)
  • Non-verbose scans print the location, type and redacted match of every finding instead of only a count
  • GitHub classic tokens are validated against their built-in CRC32 checksum, so base62 lookalike strings are rejected offline
  • UTF-16 files and staged blobs with a byte-order mark are decoded and scanned instead of being skipped as binary
  • Base64 runs of 24 or more characters are decoded and their text is scanned once more, so an encoded credential cannot pass unseen
  • --trusted-detectors and --no-repo-config split the two behaviors of --no-config-discovery, which remains as their combination
  • --max-file-size <MB> skips larger files and reports them as unscannable

Changed

  • Pre-commit hooks scan the staged diff instead of whole files
  • Config discovery searches parent directories up to the repository root
  • Hook messages abbreviate the home directory as ~
  • Findings for the same file, line and matched text collapse to the highest severity across all scan modes, so overlapping detectors report a secret once
  • Reports redact matched text by default; --show-secrets opts into raw values, and matches shorter than 8 characters are always described by length only
  • Reports summarise exclusions as a count plus a sample instead of listing every path, and report git-rendered binary files as unscannable rather than excluded
  • Lockfiles (Cargo.lock, package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, npm-shrinkwrap.json, go.sum, and other generated manifests) are excluded from scans by default
  • A directory that cannot be listed is reported as unscannable, so --fail-on-unscannable catches it
  • KeyWatch warns on stderr when an external detector file replaces the embedded set, when config overrides disable detectors, and when KEYWATCH_CONFIG_PATH is set but ignored
  • Stripe publishable keys (pk_) report as LOW under their own detector; rk_ restricted keys are detected as secrets
  • SARIF reports no longer claim precision: very-high for every rule and omit semanticVersion when unknown

Fixed

  • scan --git-history applies --exclude, skips the baseline file, and reports real file paths instead of a synthetic <git-history> key that no baseline could match
  • scan --staged is not fooled by diff.relative, which made git drop changes outside the current directory
  • --output files are readable only by their owner, including when the file already existed with wider permissions
  • Config is not trusted from a world-writable directory or file, so a .keywatch.toml dropped in /tmp cannot weaken scans beneath it
  • KEYWATCH_CONFIG_PATH is ignored in trusted mode whenever it points inside the tree being scanned, wherever the process runs from
  • Baseline suppression reports how many findings it hid, instead of applying silently
  • CreditCardDetector requires an issuer prefix and a valid Luhn checksum, instead of matching any 13-16 digit run; Discover's 644-649 and 65 ranges are covered
  • HighEntropyDetector could never fire (its 4.0 threshold is the ceiling for hex) and now runs, restricted to lines naming a credential
  • PKCS#8 private key headers (BEGIN PRIVATE KEY, BEGIN ENCRYPTED PRIVATE KEY) are detected
  • PhoneNumberDetector needs punctuation or a country code, so unix timestamps are not phone numbers
  • Detectors can require a structural check via validate = "luhn"
  • Hooks use built-in detectors, so a detectors.toml committed to a scanned repository can no longer replace the detector set and disable its own scan
  • Files git renders as binary (including text marked -diff in .gitattributes) are read from the index instead of being reported clean
  • Base64Detector matches from 28 characters, the length where entropy can actually separate base64 from identifiers
  • scan --staged no longer misses findings under color.ui = always or custom diff prefixes
  • Non-UTF-8 files no longer abort a staged scan
  • A malformed diff hunk header is reported instead of silently attributing its findings to line 0
  • Diff paths that git quoted (names containing quotes or control characters) are unescaped before attribution
  • The baseline file is no longer scanned as input to itself, including staged scans run from a subdirectory
  • GenericKeyValueDetector and RandomString no longer flag code identifiers (let payment_method_token = card_token, snake_case serde attributes)
  • PasswordDetector no longer flags $PWD:
  • GenericKeyValueDetector no longer flags bare CamelCase type paths (token: PaymentTokenData,)
  • PasswordDetector no longer flags Rust expressions (password: Secret<String>, password: config.password.clone(), Some(Secret::new(...))) as credentials
  • A failed git cat-file during a staged scan reports itself instead of claiming git diff failed
  • Custom rules in .keywatch.toml support allowlist, keywords, entropy and validate, matching built-in detector definitions
  • Pre-push repository filters fail closed on Windows drive-path remotes instead of misparsing the drive letter as a host
  • Chunked streaming scans no longer duplicate multiline matches that land inside the window overlap
  • GenericKeyValueDetector reaches its auth and _key pattern branches; the keyword prefilter previously never let auth = ... or encryption_key = ... lines reach the regex
  • NewRelicAPIKeyDetector matches real uppercase NRAK keys; the lowercase-only class could never fire
  • SlackTokenDetector keyword and pattern prefixes agree; xoxa- tokens were unreachable and xoxr- was inert
  • 48+ character sk- tokens report as OpenAI keys instead of being mislabeled Kimi/Moonshot
  • The same file passed under two spellings (dup.txt ./dup.txt) reports each finding once
  • Errors from failed git diff/git log include one summarized stderr line instead of letting git dump pages of usage text
  • --fail-on-unscannable names the unscannable files in its summary instead of printing "No secrets found." next to exit code 1
  • The AWS documentation example secret in .env.example files is no longer flagged by GenericKeyValueDetector
  • The composite Action publishes its effective exit code on report failures instead of a stale 0, and findings-count stays numeric (-1 when unknown)
  • Files with invalid UTF-8 are decoded lossily and scanned instead of silently skipped; NUL-containing files are reported as unscannable
  • Finding's plugin_name field is now detector_name in the code; the JSON report and baseline schema still emit/accept plugin_name
  • CustomRule.description is accepted and ignored so configuration files written for earlier releases keep parsing (the text was never surfaced)
  • False-positive reductions in the built-in detectors: AWS's documentation example key, placeholder values (changeme, your-api-key-here, replace-me-please), RFC 2606 example-domain emails and noreply conventions, fictional 555 phone numbers, npm/shield checksum prefixes, and non-Verhoeff 12-digit runs no longer report as Aadhaar
  • --baseline naming a missing file is an error instead of silently scanning with an empty baseline
  • Baseline files with an unknown format version are rejected instead of silently accepted
  • --update-baseline refreshes the recorded line numbers of entries it already knows, and saved baselines end with a newline
  • SARIF report property order is deterministic
  • Piping output to a closed reader no longer panics, including hook install and init; hook commands now report real output failures instead of discarding them
  • Trusted scans (--no-config-discovery) no longer read detector configuration from environment-derived locations ($XDG_CONFIG_HOME, $HOME, the executable directory), so a redirected home directory cannot replace the built-in detector set
  • Staged blobs are resolved to object IDs and read with git cat-file blob <oid> instead of :<path>, so a file whose name resembles a git stage path (0:config) can no longer substitute another file's content
  • scan --git-history reads merge commits (--diff-merges=first-parent), so a secret introduced while resolving a conflict is reported
  • Files that fail to open or read are counted as unscannable instead of being silently skipped
  • Ten detectors that matched the wrong shape or nothing at all (SupabaseServiceRoleKey, TerraformCloudToken, AzureStorageKey, DockerHubToken, CircleCIToken, DiscordToken, NetlifyToken, CodecovToken, AdyenAPIKey, RazorpayKey) now follow the documented token format, each pinned by a real-format fixture test; the two that matched only non-secrets were removed
  • Email, PhoneNumber, IPAddress, TwilioAPIKey and MailgunAPIKey no longer suppress or match unintended text: the example-domain allowlist is anchored, only the reserved 555-01xx numbers are ignored, every IPv4 octet is validated, and embedded vendor prefixes require word boundaries
  • Entropy and validators run on the captured value rather than the whole match, so api_key = "aaaaaaaaaa" is no longer reported; GCPServiceAccountKey requires a private_key field and MasterAPIKey, AzureDevOpsPAT, KimiMoonshotAPIKey and CertificateDetector severities now reflect credential impact

Performance

  • Keyword matching uses a single Aho-Corasick pass per line: ~3x faster file scans, ~9x faster streams
  • File scans stream line by line instead of reading whole files into memory
  • ~2.5x faster file scans: one combined prefilter pass for the keywordless detectors, an ASCII fast path for line lowering, and a byte-histogram entropy check that no longer allocates per match