diff --git a/.github/workflows/release-provenance.yml b/.github/workflows/release-provenance.yml deleted file mode 100644 index c3adb37..0000000 --- a/.github/workflows/release-provenance.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: SLSA Provenance - -on: - release: - types: [published] - -permissions: {} - -jobs: - # Compute SHA256 hashes of all release assets - hash: - name: Compute artifact hashes - runs-on: ubuntu-24.04 - permissions: - contents: read - outputs: - hashes: ${{ steps.hash.outputs.hashes }} - steps: - - name: Download release assets - env: - GH_TOKEN: ${{ github.token }} - TAG_NAME: ${{ github.event.release.tag_name }} - run: | - set -euo pipefail - mkdir -p assets - gh release download "${TAG_NAME}" \ - --repo "${{ github.repository }}" \ - --dir assets - - - name: Compute SHA256 hashes - id: hash - run: | - set -euo pipefail - cd assets - # Base64-encode the SHA256 hashes for SLSA provenance - echo "hashes=$(sha256sum -- * | base64 -w0)" >> "$GITHUB_OUTPUT" - - # Generate SLSA provenance for the release artifacts - provenance: - name: Generate SLSA provenance - needs: [hash] - permissions: - actions: read - id-token: write - contents: write - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 - with: - base64-subjects: "${{ needs.hash.outputs.hashes }}" - upload-assets: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ea626f..6a4fb41 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -408,6 +408,57 @@ jobs: with: subject-path: 'target/release/*' + # ──────────────── SLSA Provenance ──────────────── + hash: + name: Compute artifact hashes + needs: [release] + runs-on: ubuntu-24.04 + permissions: + contents: read + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + steps: + - name: Download release assets + env: + GH_TOKEN: ${{ github.token }} + TAG_NAME: ${{ needs.release.outputs.tag }} + run: | + set -euo pipefail + mkdir -p assets + gh release download "${TAG_NAME}" \ + --repo "${{ github.repository }}" \ + --dir assets \ + --pattern '*.tgz' \ + --pattern '*.deb' \ + --pattern '*.rpm' \ + --pattern '*.zip' + + - name: Compute SHA256 hashes + id: hash + run: | + set -euo pipefail + cd assets + shopt -s nullglob + files=( * ) + if [ ${#files[@]} -eq 0 ]; then + echo "Error: no release assets found to hash in $(pwd)" >&2 + exit 1 + fi + hashes=$(sha256sum -- "${files[@]}" | base64 -w0) + echo "hashes=${hashes}" >> "$GITHUB_OUTPUT" + + provenance: + name: Generate SLSA provenance + needs: [hash] + permissions: + actions: read + id-token: write + contents: write + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 + with: + base64-subjects: "${{ needs.hash.outputs.hashes }}" + upload-assets: true + # ──────────────── Publish Docker image ──────────────── publish-docker: needs: [release] diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4462f..df09a58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## [v1.92.0] +- Added new built-in rules for Etsy, Flutterwave, Freemius, JFrog, Kraken, KuCoin, Trello, Octopus Deploy, OpenShift, Private AI, SettleMint, Sidekiq, and Polymarket. +- Added live HTTP validation for Etsy, JFrog, Octopus Deploy, OpenShift, and Private AI where provider documentation supported reliable token-only checks. +- Added detection + validation rules for Anthropic Admin, Azure Speech, Azure Translator, Databento, DataStax Astra, DevCycle, Fullstory, GC Notify, and Stytch; built-in runtime rule count is now 601 with `--confidence=low`. +- Added Heroku token revocation support for both legacy UUID-format tokens and `HRKU-` platform tokens via the OAuth authorizations API. +- Added `hmac_sha256_b64key` Liquid filter for HMAC-SHA256 signing with base64-encoded keys (decodes key to raw bytes before signing), enabling correct Azure Notification Hub SAS validation. +- Integrated SLSA v3 provenance generation into the release workflow; hash computation now scopes to build artifacts only for idempotent re-runs. +- Removed Zapier webhook live validation (GET to a catch hook triggers the Zap). +- Hardened Heroku revocation regex to prevent crossing JSON object boundaries when extracting authorization IDs. +- Fixed Zendesk subdomain regex to reject trailing hyphens; renamed `ZENDESK_SUBDOMAIN` to `ZENDESK_HOST` for clarity. +- Fixed Stytch and Polymarket trailing `\b` boundaries that prevented matching base64-padded secrets ending with `=`. +- Tightened Kubernetes API Server URL pattern to require kube-specific identifiers, preventing bootstrap tokens from binding to unrelated `server:` entries. + ## [v1.91.0] - Added SSRF protection for credential validation: outbound HTTP requests now block connections to loopback, private, link-local, and other non-public IP addresses. HTTP redirect targets are DNS-resolved and validated against the same SSRF rules. Use `--allow-internal-ips` to opt out when scanning internal infrastructure. - Consolidated JWT SSRF checks to use the shared `is_ssrf_safe_ip` function, covering additional reserved ranges (CGNAT, documentation, benchmarking, IPv6 unique-local). diff --git a/Cargo.toml b/Cargo.toml index 162bc34..f37bf5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ http = "1.4" [package] name = "kingfisher" -version = "1.91.0" +version = "1.92.0" description = "MongoDB's blazingly fast and accurate secret scanning and validation tool" edition.workspace = true rust-version.workspace = true diff --git a/NOTICE b/NOTICE index b95e1b1..bd6e957 100644 --- a/NOTICE +++ b/NOTICE @@ -47,10 +47,16 @@ Certain detection rules: * crates/kingfisher-rules/data/rules/instagram.yml * crates/kingfisher-rules/data/rules/iterable.yml * crates/kingfisher-rules/data/rules/lokalise.yml + * crates/kingfisher-rules/data/rules/azure-notification-hub.yml + * crates/kingfisher-rules/data/rules/firebase.yml + * crates/kingfisher-rules/data/rules/helpscout.yml + * crates/kingfisher-rules/data/rules/kubernetes.yml * crates/kingfisher-rules/data/rules/pendo.yml * crates/kingfisher-rules/data/rules/razorpay.yml * crates/kingfisher-rules/data/rules/spotify.yml * crates/kingfisher-rules/data/rules/wakatime.yml + * crates/kingfisher-rules/data/rules/zapier.yml + * crates/kingfisher-rules/data/rules/zendesk.yml are derived in part from Titus (https://github.com/praetorian-inc/titus), which is licensed under the Apache License, Version 2.0. @@ -72,3 +78,73 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +-------------------------------------------------------------------- + +Certain detection rules: + * crates/kingfisher-rules/data/rules/octopusdeploy.yml + * crates/kingfisher-rules/data/rules/openshift.yml + * crates/kingfisher-rules/data/rules/polymarket.yml + * crates/kingfisher-rules/data/rules/privateai.yml + * crates/kingfisher-rules/data/rules/settlemint.yml + * crates/kingfisher-rules/data/rules/sidekiq.yml + +are derived in part from Betterleaks +(https://github.com/betterleaks/betterleaks), which is licensed under the MIT +License. + +Betterleaks +Copyright (c) 2026 Zachary Rice + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------- + +Certain detection rules: + * crates/kingfisher-rules/data/rules/etsy.yml + * crates/kingfisher-rules/data/rules/flutterwave.yml + * crates/kingfisher-rules/data/rules/freemius.yml + * crates/kingfisher-rules/data/rules/jfrog.yml + * crates/kingfisher-rules/data/rules/kraken.yml + * crates/kingfisher-rules/data/rules/kucoin.yml + * crates/kingfisher-rules/data/rules/trello.yml + +are derived in part from Gitleaks (https://github.com/gitleaks/gitleaks), +which is licensed under the MIT License. + +Gitleaks +Copyright (c) 2019 Zachary Rice + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d5cb266..c7c1d4f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Kingfisher Logo [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![Detection Rules](https://img.shields.io/badge/Detection%20Rules-548-2ea043.svg)](https://github.com/mongodb/kingfisher)
+[![Detection Rules](https://img.shields.io/badge/Detection%20Rules-601-2ea043.svg)](https://github.com/mongodb/kingfisher)
[![ghcr downloads](https://ghcr-badge.elias.eu.org/shield/mongodb/kingfisher/kingfisher)](https://github.com/mongodb/kingfisher/pkgs/container/kingfisher)
@@ -302,6 +302,40 @@ Kingfisher supports multiple installation methods: **For complete installation instructions and pre-commit hook setup, see [docs/INSTALLATION.md](docs/INSTALLATION.md).** +## Verifying Releases + +Every Kingfisher release includes [SLSA v3](https://slsa.dev) provenance and GitHub build attestations so you can verify that artifacts were built by our CI pipeline and haven't been tampered with. + +### SLSA provenance + +Each GitHub release includes a `multiple.intoto.jsonl` provenance file. Verify any release artifact with [`slsa-verifier`](https://github.com/slsa-framework/slsa-verifier): + +```bash +# Install slsa-verifier +go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest + +# Download the artifact and provenance from the release +gh release download --repo mongodb/kingfisher \ + --pattern 'kingfisher-linux-x64.tgz' \ + --pattern 'multiple.intoto.jsonl' + +# Verify +slsa-verifier verify-artifact kingfisher-linux-x64.tgz \ + --provenance-path multiple.intoto.jsonl \ + --source-uri github.com/mongodb/kingfisher +``` + +### GitHub attestations + +Release artifacts also have GitHub build attestations, verifiable with the GitHub CLI: + +```bash +gh release download --repo mongodb/kingfisher \ + --pattern 'kingfisher-linux-x64.tgz' + +gh attestation verify kingfisher-linux-x64.tgz --repo mongodb/kingfisher +``` + # Detection Rules Kingfisher ships with [hundreds of rules](crates/kingfisher-rules/data/rules/) that cover everything from classic cloud keys to the latest AI SaaS tokens. Below is an overview: diff --git a/crates/kingfisher-rules/data/rules/AGENTS.md b/crates/kingfisher-rules/data/rules/AGENTS.md index 80a9522..80ecbaf 100644 --- a/crates/kingfisher-rules/data/rules/AGENTS.md +++ b/crates/kingfisher-rules/data/rules/AGENTS.md @@ -30,8 +30,27 @@ Strongly recommended fields: ## Pattern Quality Rules - Prefer specific anchors/prefixes and provider context over broad generic regex. +- When the token format is generic or common-looking (for example bare 32-hex keys), prefer contextual patterns of the form: provider keyword -> short flexible gap -> key/secret label -> short flexible gap -> token. A good default is: + - `\b` + - provider identifier (for example `amplitude`, `azure`, `speech`, `translator`) + - `(?:.|[\n\r]){0,N}?` + - common credential labels such as `(?:SECRET|PRIVATE|ACCESS|KEY|TOKEN|AUTHORIZATION|API)` + - `(?:.|[\n\r]){0,M}?` + - the token capture wrapped in a single unnamed capture group +- Do not add surrounding context when the token is already strongly self-identifying by prefix or structure (for example `sk-ant-api...`, `AstraCS:...`, `dvc_client_...`, `secret-test-...`). In those cases, prefer the tighter self-identifying regex. - Use `pattern_requirements` to enforce quality constraints (`min_digits`, `min_uppercase`, `min_lowercase`, `min_special_chars`, `ignore_if_contains`, `checksum`). -- Use checksum validation in `pattern_requirements.checksum` when token formats support it. +- Use checksum validation in `pattern_requirements.checksum` when token formats support it. This is preferred when the provider token format includes a documented or reverse-engineered check segment, because it can sharply reduce false positives without adding brittle surrounding context. +- For checksum-based rules, prefer named captures for the main token body and checksum suffix/prefix, then compute the expected checksum in Liquid. A typical pattern is: + - `( + prefix_(?P...)(?P...) + )` + - with: + - `actual.template: "{{ checksum }}"` + - `actual.requires_capture: checksum` + - `expected: "{{ body | | }}"` + - `skip_if_missing: true` +- Example: GitHub PATs use a CRC32-derived base62 checksum. The rule in `github.yml` captures `body` and `checksum`, then compares `{{ checksum }}` against `{{ body | crc32 | base62: 6 }}`. +- Prefer checksum validation over extra loose context whenever the token structure itself supports it. If the checksum is only present on some token generations, keep `skip_if_missing: true` so older examples continue to load safely. - Use `visible: false` for helper/non-secret captures used only by dependent rules. - Use `depends_on_rule` for multi-part credential validation (for example ID + secret). diff --git a/crates/kingfisher-rules/data/rules/anthropic.yml b/crates/kingfisher-rules/data/rules/anthropic.yml index 3aed2b1..1a2784a 100644 --- a/crates/kingfisher-rules/data/rules/anthropic.yml +++ b/crates/kingfisher-rules/data/rules/anthropic.yml @@ -47,4 +47,38 @@ rules: words: - '"type":"message"' - 'credit balance is too low' - url: https://api.anthropic.com/v1/messages \ No newline at end of file + url: https://api.anthropic.com/v1/messages + + - name: Anthropic Admin API Key + id: kingfisher.anthropic.2 + pattern: | + (?xi) + ( + sk-ant-admin(?:\d{2,4})?-[A-Za-z0-9_-]{40,} + ) + pattern_requirements: + min_digits: 2 + min_uppercase: 1 + min_lowercase: 1 + min_entropy: 3.5 + confidence: medium + examples: + - sk-ant-admin03-4mB9zY2Qx8LmN7pR5sT1uV6wX0aBcDeFgHiJkLmNoPqRsTuVwXyZ1234 + references: + - https://docs.anthropic.com/en/api/administration-api + - https://docs.anthropic.com/en/api/admin-api/organization/get-me + validation: + type: Http + content: + request: + method: GET + url: https://api.anthropic.com/v1/organizations/me + headers: + x-api-key: '{{ TOKEN }}' + anthropic-version: "2023-06-01" + content-type: application/json + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: JsonValid diff --git a/crates/kingfisher-rules/data/rules/azure-notification-hub.yml b/crates/kingfisher-rules/data/rules/azure-notification-hub.yml new file mode 100644 index 0000000..28ca7bc --- /dev/null +++ b/crates/kingfisher-rules/data/rules/azure-notification-hub.yml @@ -0,0 +1,156 @@ +rules: + - name: Azure Notification Hub Namespace Host + id: kingfisher.azure.notificationhub.1 + pattern: | + (?xi) + \b + (?: + endpoint + \s*=\s* + sb:// + | + notification + (?:.|[\n\r]){0,48}? + https:// + ) + ( + [a-z0-9] + [a-z0-9-]{1,62} + \.servicebus\.windows\.net + ) + (?:/|;|\b) + min_entropy: 2.0 + confidence: medium + visible: false + examples: + - Endpoint=sb://acme-push.servicebus.windows.net/;SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey=VGhpcytpcythK3Rlc3Qra2V5K3ZhbHVlLzEyMzQ1Njc4OTA= + - 'notificationHubEndpoint: "https://mobile-prod.servicebus.windows.net"' + references: + - https://learn.microsoft.com/en-us/rest/api/notificationhubs/use-rest-api-backend + + - name: Azure Notification Hub Name + id: kingfisher.azure.notificationhub.2 + pattern: | + (?xi) + \b + (?: + notification + (?:hub)? + (?:name|path) + | + hub + (?:name|path) + ) + \s*[:=]\s* + ["']? + ( + [A-Za-z0-9] + [A-Za-z0-9._-]{6,63} + ) + ["']? + \b + min_entropy: 2.0 + confidence: medium + visible: false + examples: + - NotificationHubPath=my-mobile-hub + - 'notificationHubName: "android-prod"' + references: + - https://learn.microsoft.com/en-us/azure/notification-hubs/create-notification-hub-portal + + - name: Azure Notification Hub SAS Key Name + id: kingfisher.azure.notificationhub.3 + pattern: | + (?xi) + \b + ["']? + SharedAccessKeyName + ["']? + \s*[:=]\s* + ["']? + ( + [A-Za-z] + [A-Za-z0-9_-]{6,63} + ) + ["']? + \b + min_entropy: 2.0 + confidence: medium + visible: false + examples: + - SharedAccessKeyName=DefaultListenSharedAccessSignature + - '"SharedAccessKeyName": "DefaultFullSharedAccessSignature"' + references: + - https://learn.microsoft.com/en-us/azure/notification-hubs/notification-hubs-push-notification-security + + - name: Azure Notification Hub Access Key + id: kingfisher.azure.notificationhub.4 + pattern: | + (?xi) + (?: + (?:notification\s*hub|Endpoint\s*=\s*sb://[a-z0-9-]{2,63}\.servicebus\.windows\.net/?) + (?:.|[\n\r]){0,160}? + SharedAccessKey + | + \b + (?:hubAccessKey|notificationHub(?:Access)?Key) + \b + ) + \s*[:=]\s* + ["']? + ( + [A-Za-z0-9+/]{32,88}={0,2} + ) + ["']? + (?:[^A-Za-z0-9+/=]|$) + pattern_requirements: + min_digits: 2 + min_uppercase: 1 + min_lowercase: 1 + ignore_if_contains: + - example + - sample + - document + - placeholder + min_entropy: 3.7 + confidence: medium + examples: + - Endpoint=sb://acme-push.servicebus.windows.net/;SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey=Q29udG9zb1Rlc3RLZXkrMTIzNDU2Nzg5MC9BQkNERUZHSEk= + - | + const config = { + notificationHubName: "android-prod", + hubAccessKey: "U2FmZUtleVZhbHVlKzEyMzQ1Njc4OTBBQkNERUYrLz09" + }; + references: + - https://learn.microsoft.com/en-us/azure/notification-hubs/notification-hubs-push-notification-security + - https://learn.microsoft.com/en-us/rest/api/notificationhubs/use-rest-api-backend + depends_on_rule: + - rule_id: kingfisher.azure.notificationhub.1 + variable: NH_HOST + - rule_id: kingfisher.azure.notificationhub.2 + variable: NH_HUB + - rule_id: kingfisher.azure.notificationhub.3 + variable: NH_KEY_NAME + validation: + type: Http + content: + request: + method: GET + url: 'https://{{ NH_HOST }}/{{ NH_HUB }}/registrations/?api-version=2015-01' + headers: + Accept: application/atom+xml + Authorization: | + {%- assign uri = "https://" | append: NH_HOST | append: "/" | append: NH_HUB | append: "/registrations/?api-version=2015-01" -%} + {%- assign se = "" | unix_timestamp | plus: 300 -%} + {%- assign nl = "" | newline -%} + {%- assign to_sign = uri | url_encode | append: nl | append: se -%} + {%- capture auth -%}SharedAccessSignature sr={{ uri | url_encode }}&sig={{ to_sign | hmac_sha256_b64key: TOKEN | url_encode }}&se={{ se }}&skn={{ NH_KEY_NAME | url_encode }}{%- endcapture -%} + {{ auth | strip_newlines }} + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: XmlValid + - type: WordMatch + words: + - "\s* + ["'] + ( + sk_[^"' \t\r\n]{29} + ) + ["'] + pattern_requirements: + min_digits: 2 + min_lowercase: 6 + min_special_chars: 2 + ignore_if_contains: + - xxxxxxxxx + - placeholder + min_entropy: 3.6 + confidence: medium + examples: + - | + $config = array( + "secret_key" => "sk_ubb4yN3mzqGR2x8#P7r5&@*xC$utE", + ); + references: + - https://freemius.com/help/documentation/wordpress-sdk/integrating-freemius-sdk/ + diff --git a/crates/kingfisher-rules/data/rules/fullstory.yml b/crates/kingfisher-rules/data/rules/fullstory.yml new file mode 100644 index 0000000..e8783b1 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/fullstory.yml @@ -0,0 +1,41 @@ +rules: + - name: Fullstory API Key + id: kingfisher.fullstory.1 + pattern: | + (?xi) + \b + (?:fullstory|fs_api|fullstory_api) + (?:.|[\n\r]){0,32}? + (?:SECRET|PRIVATE|ACCESS|KEY|TOKEN|AUTHORIZATION|API) + (?:.|[\n\r]){0,16}? + \b + ( + (?:na1|eu1)\.[A-Za-z0-9]{20,} + ) + \b + pattern_requirements: + min_digits: 2 + min_lowercase: 2 + min_entropy: 3.3 + confidence: medium + examples: + - FULLSTORY_API_KEY=na1.Abcd1234Efgh5678Ijkl9012Mnop3456 + - 'fs_api_key: "eu1.Abcd1234Efgh5678Ijkl9012Mnop3456"' + references: + - https://developer.fullstory.com/server/v1/getting-started/ + - https://developer.fullstory.com/server/authentication/ + - https://developer.fullstory.com/server/v1/authentication/me/ + validation: + type: Http + content: + request: + method: GET + url: https://api.fullstory.com/me + headers: + Authorization: "Basic {{ TOKEN | append: ':' | b64enc }}" + Accept: application/json + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: JsonValid diff --git a/crates/kingfisher-rules/data/rules/gcnotify.yml b/crates/kingfisher-rules/data/rules/gcnotify.yml new file mode 100644 index 0000000..1d83850 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/gcnotify.yml @@ -0,0 +1,41 @@ +rules: + - name: GC Notify API Key + id: kingfisher.gcnotify.1 + pattern: | + (?xi) + \b + ( + ApiKey-v1 + \s+ + gcntfy-[a-z0-9_]+ + - + [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} + - + [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} + ) + \b + pattern_requirements: + min_digits: 4 + min_lowercase: 2 + min_entropy: 3.5 + confidence: medium + examples: + - 'Authorization: "ApiKey-v1 gcntfy-my_test_key-26785a09-ab16-4eb0-8407-a37497a57506-3d844edf-8d35-48ac-975b-e847b4f122b0"' + references: + - https://documentation.notification.canada.ca/en/start.html + - https://documentation.notification.canada.ca/en/status.html + - https://documentation.notification.canada.ca/en/keys.html + validation: + type: Http + content: + request: + method: GET + url: https://api.notification.canada.ca/v2/notifications + headers: + Authorization: "{{ TOKEN }}" + Accept: application/json + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: JsonValid diff --git a/crates/kingfisher-rules/data/rules/helpscout.yml b/crates/kingfisher-rules/data/rules/helpscout.yml new file mode 100644 index 0000000..6a63bb7 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/helpscout.yml @@ -0,0 +1,82 @@ +rules: + - name: Help Scout Client ID + id: kingfisher.helpscout.1 + pattern: | + (?xi) + \b + (?:help[\s_-]?scout|helpscout) + (?:.|[\n\r]){0,48}? + (?:client[\s_.-]*id|app[\s_.-]*id|application[\s_.-]*id) + (?:.|[\n\r]){0,16}? + ( + [A-Za-z0-9]{10,40} + ) + \b + pattern_requirements: + min_digits: 1 + min_uppercase: 1 + min_lowercase: 1 + min_entropy: 3.1 + confidence: medium + visible: false + examples: + - HELPSCOUT_CLIENT_ID=Ab12Cd34Ef56Gh78Ij90 + - 'helpscout_app_id: "a1B2c3D4e5F6g7H8i9J0K1L2"' + references: + - https://developer.helpscout.com/mailbox-api/ + + - name: Help Scout OAuth Client Secret + id: kingfisher.helpscout.2 + pattern: | + (?xi) + \b + (?:help[\s_-]?scout|helpscout) + (?:.|[\n\r]){0,48}? + (?: + client[\s_.-]*secret + | + app[\s_.-]*secret + | + oauth[\s_.-]*secret + ) + (?:.|[\n\r]){0,16}? + ( + [A-Za-z0-9]{24,64} + ) + \b + pattern_requirements: + min_digits: 2 + min_uppercase: 1 + min_lowercase: 1 + ignore_if_contains: + - example + - placeholder + - yoursecret + min_entropy: 3.7 + confidence: medium + examples: + - HELPSCOUT_CLIENT_SECRET=a3B8f29E4d1C6a0578e23D9f41b6C8e2 + - 'helpscout_client_secret: "E7d2A1f849c3B05d6e81F2a794c3D5b0"' + references: + - https://developer.helpscout.com/mailbox-api/ + depends_on_rule: + - rule_id: kingfisher.helpscout.1 + variable: CLIENT_ID + validation: + type: Http + content: + request: + method: POST + url: https://api.helpscout.net/v2/oauth2/token + headers: + Content-Type: application/x-www-form-urlencoded + Accept: application/json + body: 'grant_type=client_credentials&client_id={{ CLIENT_ID | url_encode }}&client_secret={{ TOKEN | url_encode }}' + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: JsonValid + - type: WordMatch + words: + - '"access_token"' diff --git a/crates/kingfisher-rules/data/rules/heroku.yml b/crates/kingfisher-rules/data/rules/heroku.yml index 96e94e4..c929e67 100644 --- a/crates/kingfisher-rules/data/rules/heroku.yml +++ b/crates/kingfisher-rules/data/rules/heroku.yml @@ -20,6 +20,7 @@ rules: - 'HEROKU_API_KEY: c55dbac4-e0e8-4a06-b892-75cac2387ce5' references: - https://devcenter.heroku.com/articles/authentication + - https://devcenter.heroku.com/articles/oauth validation: type: Http content: @@ -33,6 +34,35 @@ rules: - report_response: true - type: StatusMatch status: [200] + revocation: + type: HttpMultiStep + content: + steps: + - name: lookup_authorization_id + request: + method: GET + url: https://api.heroku.com/oauth/authorizations + headers: + Accept: application/vnd.heroku+json; version=3 + Authorization: Bearer {{ TOKEN }} + response_matcher: + - type: StatusMatch + status: [200] + extract: + AUTHORIZATION_ID: + type: Regex + pattern: '"id":"([^"]+)"[^{}]{0,2048}?"token":"{{ TOKEN }}"' + - name: revoke_authorization + request: + method: DELETE + url: https://api.heroku.com/oauth/authorizations/{{ AUTHORIZATION_ID }} + headers: + Accept: application/vnd.heroku+json; version=3 + Authorization: Bearer {{ TOKEN }} + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] - name: Heroku API Key (Platform Key) id: kingfisher.heroku.2 pattern: | @@ -61,8 +91,38 @@ rules: - '"id":' - '"name":' match_all_words: true + revocation: + type: HttpMultiStep + content: + steps: + - name: lookup_authorization_id + request: + method: GET + url: https://api.heroku.com/oauth/authorizations + headers: + Accept: application/vnd.heroku+json; version=3 + Authorization: Bearer {{TOKEN}} + response_matcher: + - type: StatusMatch + status: [200] + extract: + AUTHORIZATION_ID: + type: Regex + pattern: '"id":"([^"]+)"[^{}]{0,2048}?"token":"{{ TOKEN }}"' + - name: revoke_authorization + request: + method: DELETE + url: https://api.heroku.com/oauth/authorizations/{{ AUTHORIZATION_ID }} + headers: + Accept: application/vnd.heroku+json; version=3 + Authorization: "Bearer {{TOKEN}}" + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] references: - https://devcenter.heroku.com/articles/platform-api-quickstart + - https://devcenter.heroku.com/articles/oauth examples: - "HRKU-AADVTUYvfjT4nhuJ07bEfAUq9GS3PkTdyWuNBiXYmYMg_____wgAf6OTnGyh" - "HRKU-AABW9W1iH9NHEIlAABq9nZUq9GS3PkTdyWuNBiXYmYMg_____wV2XYIXxm5p" diff --git a/crates/kingfisher-rules/data/rules/jfrog.yml b/crates/kingfisher-rules/data/rules/jfrog.yml new file mode 100644 index 0000000..4285f49 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/jfrog.yml @@ -0,0 +1,129 @@ +rules: + - name: JFrog Cloud Host + id: kingfisher.jfrog.1 + pattern: | + (?xi) + \b + ( + [a-z0-9] + (?: + [a-z0-9\-]{0,61} + [a-z0-9] + )? + \.jfrog\.io + ) + \b + min_entropy: 2.5 + confidence: medium + visible: false + examples: + - company.jfrog.io + - my-team.jfrog.io + references: + - https://jfrog.com/help/api/khub/documents/xrOb4ANk_fqUw5nctnsIww/content + + - name: JFrog API Key + id: kingfisher.jfrog.2 + pattern: | + (?xi) + \b + (?: + jfrog | + artifactory | + bintray | + xray + ) + (?:.|[\n\r]){0,32}? + (?: + api[_-]?key | + password | + token | + secret + ) + (?:.|[\n\r]){0,12}? + ( + [A-Za-z0-9]{73} + ) + \b + pattern_requirements: + min_digits: 4 + min_uppercase: 2 + min_lowercase: 6 + min_entropy: 3.5 + confidence: medium + examples: + - jfrog_api_key=Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56Ab78Cd90Ef12Gh34Ij5Kl + - jfrog_password=Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56Ab78Cd90Ef12Gh34Ij5Kl + references: + - https://jfrog.com/help/api/khub/documents/xrOb4ANk_fqUw5nctnsIww/content + - https://jfrog.com/article/access-service/ + depends_on_rule: + - rule_id: kingfisher.jfrog.1 + variable: JFROG_HOST + validation: + type: Http + content: + request: + method: GET + url: https://{{ JFROG_HOST }}/artifactory/api/repositories + headers: + X-JFrog-Art-Api: '{{ TOKEN }}' + Accept: application/json + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: JsonValid + + - name: JFrog Identity Token + id: kingfisher.jfrog.3 + pattern: | + (?xi) + \b + (?: + jfrog | + artifactory | + bintray | + xray + ) + (?:.|[\n\r]){0,32}? + (?: + identity[_-]?token | + access[_-]?token | + bearer | + token + ) + (?:.|[\n\r]){0,12}? + ( + [A-Za-z0-9]{64} + ) + \b + pattern_requirements: + min_digits: 4 + min_uppercase: 2 + min_lowercase: 6 + min_entropy: 3.4 + confidence: medium + examples: + - jfrog_identity_token=Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56Ab78Cd90Ef12 + - artifactory_access_token=Zx12Cv34Bn56Mm78Aa90Ss12Dd34Ff56Gg78Hh90Jj12Kk34Ll56Qq78Ww90Ee12 + references: + - https://jfrog.com/help/api/khub/documents/xrOb4ANk_fqUw5nctnsIww/content + - https://jfrog.com/article/access-service/ + depends_on_rule: + - rule_id: kingfisher.jfrog.1 + variable: JFROG_HOST + validation: + type: Http + content: + request: + method: GET + url: https://{{ JFROG_HOST }}/artifactory/api/repositories + headers: + Authorization: 'Bearer {{ TOKEN }}' + Accept: application/json + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: JsonValid diff --git a/crates/kingfisher-rules/data/rules/kraken.yml b/crates/kingfisher-rules/data/rules/kraken.yml new file mode 100644 index 0000000..bd3fc2e --- /dev/null +++ b/crates/kingfisher-rules/data/rules/kraken.yml @@ -0,0 +1,31 @@ +rules: + - name: Kraken API Secret + id: kingfisher.kraken.1 + pattern: | + (?xi) + \b + kraken + (?:.|[\n\r]){0,32}? + (?: + api[_-]?secret | + secret | + private[_-]?key | + token + ) + (?:.|[\n\r]){0,12}? + ( + [A-Za-z0-9+/=_-]{80,90} + ) + (?:[^A-Za-z0-9+/=_-]|$) + pattern_requirements: + min_digits: 4 + min_uppercase: 2 + min_lowercase: 8 + min_entropy: 4.0 + confidence: medium + examples: + - KRAKEN_API_SECRET=dGhpcy1sb29rcy1saWtlLWEtYmFzZTY0LWtyYWtlbi1zZWNyZXQtdGhhdC1pcy1sb25nLWVub3VnaA== + - kraken_secret="Aq1Bq2Cr3Ds4Et5Fu6Gv7Hw8Ix9Jy0Kz1La2Mb3Nc4Od5Pe6Qf7Rg8Sh9Ti0Uj1Vk2Wm3Xn4Yo5Za6Bc7" + references: + - https://docs.kraken.com/api/docs/guides/spot-rest-auth/ + - https://docs.kraken.com/api/docs/rest-api/get-account-balance/ diff --git a/crates/kingfisher-rules/data/rules/kubernetes.yml b/crates/kingfisher-rules/data/rules/kubernetes.yml new file mode 100644 index 0000000..47d05f3 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/kubernetes.yml @@ -0,0 +1,116 @@ +rules: + - name: Kubernetes API Server URL + id: kingfisher.kubernetes.1 + pattern: | + (?xi) + \b + (?: + kube(?:rnetes)?(?:_api)?_server + | + api_server + ) + \s*[:=]\s* + ["']? + ( + https:// + (?: + \[[0-9a-f:.]+\] + | + [a-z0-9] + [a-z0-9.-]{1,253} + ) + (?::\d{2,5})? + ) + ["']? + min_entropy: 2.0 + confidence: medium + visible: false + examples: + - "kube_server: https://10.96.0.1:443" + - KUBE_API_SERVER=https://api.cluster.example.com:6443 + references: + - https://kubernetes.io/docs/reference/access-authn-authz/authentication/ + + - name: Kubernetes Bootstrap Token + id: kingfisher.kubernetes.2 + pattern: | + (?xi) + \b + (?: + bootstrap(?:[-_ ]token)? + | + tls[-_ ]bootstrap[-_ ]token + ) + (?:.|[\n\r]){0,12}? + ( + [a-z0-9]{6} + \. + [a-z0-9]{16} + ) + \b + pattern_requirements: + min_digits: 2 + min_entropy: 3.2 + confidence: medium + examples: + - BOOTSTRAP_TOKEN=be8dfd.da8a689a46edc282 + - --tls-bootstrap-token abcdef.1234567890abcdef + references: + - https://kubernetes.io/docs/reference/access-authn-authz/bootstrap-tokens/ + depends_on_rule: + - rule_id: kingfisher.kubernetes.1 + variable: KUBE_API_SERVER + validation: + type: Http + content: + request: + method: GET + url: '{{ KUBE_API_SERVER }}/api/v1/namespaces?limit=1' + headers: + Accept: application/json + Authorization: 'Bearer {{ TOKEN }}' + response_matcher: + - report_response: true + - type: StatusMatch + status: [200, 403] + - type: JsonValid + + - name: Kubernetes Bootstrap Token Pair + id: kingfisher.kubernetes.3 + pattern: | + (?xis) + \btoken-id\b + \s*:\s* + (?P[a-z0-9]{6}) + (?:.|[\n\r]){0,24}? + \btoken-secret\b + \s*:\s* + (?P[a-z0-9]{16}) + \b + pattern_requirements: + min_digits: 2 + min_entropy: 2.8 + confidence: medium + examples: + - | + token-id: 07402b + token-secret: f395accd245ae53d + references: + - https://kubernetes.io/docs/reference/access-authn-authz/bootstrap-tokens/ + depends_on_rule: + - rule_id: kingfisher.kubernetes.1 + variable: KUBE_API_SERVER + validation: + type: Http + content: + request: + method: GET + url: '{{ KUBE_API_SERVER }}/api/v1/namespaces?limit=1' + headers: + Accept: application/json + Authorization: 'Bearer {{ TOKEN_ID }}.{{ TOKEN_SECRET }}' + response_matcher: + - report_response: true + - type: StatusMatch + status: [200, 403] + - type: JsonValid diff --git a/crates/kingfisher-rules/data/rules/kucoin.yml b/crates/kingfisher-rules/data/rules/kucoin.yml new file mode 100644 index 0000000..c2d92ed --- /dev/null +++ b/crates/kingfisher-rules/data/rules/kucoin.yml @@ -0,0 +1,62 @@ +rules: + - name: KuCoin API Key + id: kingfisher.kucoin.1 + pattern: | + (?xi) + \b + kucoin + (?:.|[\n\r]){0,32}? + (?: + api[_-]?key | + key + ) + (?:.|[\n\r]){0,12}? + ( + [a-f0-9]{24} + ) + \b + pattern_requirements: + min_digits: 4 + min_lowercase: 8 + ignore_if_contains: + - xxxxxx + - your_api_key + min_entropy: 3.0 + confidence: medium + examples: + - KUCOIN_API_KEY=4f4ecb6f11b1a70001c8e2ff + - 'kucoin_api_key: a1b2c3d4e5f60718293a4b5c' + references: + - https://www.kucoin.com/docs-new/authentication + - https://www.kucoin.com/docs-new/api-3470125 + + - name: KuCoin API Secret + id: kingfisher.kucoin.2 + pattern: | + (?xi) + \b + kucoin + (?:.|[\n\r]){0,32}? + (?: + api[_-]?secret | + secret + ) + (?:.|[\n\r]){0,12}? + ( + [a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12} + ) + \b + pattern_requirements: + min_digits: 6 + min_lowercase: 8 + ignore_if_contains: + - 00000000-0000-0000-0000-000000000000 + - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + min_entropy: 3.3 + confidence: medium + examples: + - KUCOIN_API_SECRET=7d70f6c7-42e9-4261-8a8d-8ca2d5028d4f + - 'kucoin_secret: a1b2c3d4-e5f6-7890-abcd-ef1234567890' + references: + - https://www.kucoin.com/docs-new/authentication + diff --git a/crates/kingfisher-rules/data/rules/octopusdeploy.yml b/crates/kingfisher-rules/data/rules/octopusdeploy.yml new file mode 100644 index 0000000..b834bdb --- /dev/null +++ b/crates/kingfisher-rules/data/rules/octopusdeploy.yml @@ -0,0 +1,66 @@ +rules: + - name: Octopus Deploy Server URL + id: kingfisher.octopusdeploy.1 + pattern: | + (?xi) + (?: + \boctopus(?:[_\s.-]?deploy)?(?:[_\s.-]?(?:url|server|host))?\b + (?:.|[\n\r]){0,32}? + [:=] + | + \boctopus(?:url|server|host)\b + \s*[:=] + ) + \s*["']? + ( + https:// + [A-Za-z0-9.-]+ + (?::\d{2,5})? + ) + ["']? + min_entropy: 2.3 + confidence: medium + visible: false + examples: + - OCTOPUS_URL=https://deploy.acme.example + - 'octopus_server: "https://octopus.internal.example:8443"' + references: + - https://octopus.com/docs/octopus-rest-api/getting-started + + - name: Octopus Deploy API Key + id: kingfisher.octopusdeploy.2 + pattern: | + (?x) + \b + ( + API-[A-Z0-9]{26} + ) + \b + pattern_requirements: + min_digits: 4 + min_uppercase: 4 + min_entropy: 3.4 + confidence: medium + examples: + - OCTOPUS_API_KEY=API-ZNRMR7SL6L3ATMOIK7GKJDKLPY + - 'set apikey="API-A1B2C3D4E5F6G7H8J9K0LMNOPQ"' + references: + - https://octopus.com/docs/octopus-rest-api/getting-started + - https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key + depends_on_rule: + - rule_id: kingfisher.octopusdeploy.1 + variable: OCTOPUS_URL + validation: + type: Http + content: + request: + method: GET + url: '{{ OCTOPUS_URL }}/api' + headers: + X-Octopus-ApiKey: '{{ TOKEN }}' + Accept: application/json + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: JsonValid diff --git a/crates/kingfisher-rules/data/rules/openshift.yml b/crates/kingfisher-rules/data/rules/openshift.yml new file mode 100644 index 0000000..63aa3c4 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/openshift.yml @@ -0,0 +1,78 @@ +rules: + - name: OpenShift API Server URL + id: kingfisher.openshift.1 + pattern: | + (?xi) + (?: + \boc\s+login\b + (?:.|[\n\r]){0,128}? + --server= + | + \bopenshift(?:[_-]?(?:server|api(?:[_-]?server)?|cluster(?:[_-]?url)?))?\b + (?:.|[\n\r]){0,32}? + \b(?:server|api(?:[_-]?server)?|cluster(?:[_-]?url)?)?\b + \s*[:=]\s* + ) + ["']? + ( + https:// + (?: + \[[0-9a-f:.]+\] + | + [a-z0-9] + [a-z0-9.-]{1,253} + ) + (?::\d{2,5})? + ) + ["']? + min_entropy: 2.0 + confidence: medium + visible: false + examples: + - oc login --token=sha256~kV46hPnEYhCWFnB85r5NrprAxggzgb6GOeLbgcKNsH0 --server=https://api.cluster.example.com:6443 + - OPENSHIFT_SERVER=https://api.dev-cluster.example.net:6443 + references: + - https://docs.redhat.com/en/documentation/openshift_container_platform/4.9/html-single/authentication_and_authorization/index + - https://docs.redhat.com/en/documentation/openshift_container_platform/4.17/html/user_and_group_apis/user-user-openshift-io-v1 + + - name: OpenShift OAuth Access Token + id: kingfisher.openshift.2 + pattern: | + (?x) + \b + ( + sha256~[A-Za-z0-9_-]{43} + ) + (?:[^A-Za-z0-9_-]|$) + pattern_requirements: + min_digits: 3 + min_uppercase: 1 + min_lowercase: 3 + ignore_if_contains: + - put_your_token_here + - xxxxxx + min_entropy: 3.8 + confidence: medium + examples: + - 'Authorization: Bearer sha256~kV46hPnEYhCWFnB85r5NrprAxggzgb6GOeLbgcKNsH0' + - oc login --token=sha256~ZBMKw9VAayhdnyANaHvjJeXDiGwA7Fsr5gtLKj3-eh- --server=https://api.cluster.example.com:6443 + references: + - https://docs.redhat.com/en/documentation/openshift_container_platform/4.17/html/oauth_apis/oauthaccesstoken-oauth-openshift-io-v1 + - https://docs.redhat.com/en/documentation/openshift_container_platform/4.9/html-single/authentication_and_authorization/index + depends_on_rule: + - rule_id: kingfisher.openshift.1 + variable: OPENSHIFT_API_SERVER + validation: + type: Http + content: + request: + method: GET + url: '{{ OPENSHIFT_API_SERVER }}/apis/user.openshift.io/v1/users' + headers: + Authorization: 'Bearer {{ TOKEN }}' + Accept: application/json + response_matcher: + - report_response: true + - type: StatusMatch + status: [200, 403] + - type: JsonValid diff --git a/crates/kingfisher-rules/data/rules/polymarket.yml b/crates/kingfisher-rules/data/rules/polymarket.yml new file mode 100644 index 0000000..fa13685 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/polymarket.yml @@ -0,0 +1,85 @@ +rules: + - name: Polymarket Builder Secret + id: kingfisher.polymarket.1 + pattern: | + (?xi) + \b + poly(?:market)? + (?:.|[\n\r]){0,32}? + (?:builder|api)? + (?:.|[\n\r]){0,16}? + secret + (?:.|[\n\r]){0,12}? + ( + [A-Za-z0-9+/]{40,88}={0,2} + ) + (?:[^A-Za-z0-9+/=]|$) + pattern_requirements: + min_digits: 2 + min_uppercase: 1 + min_lowercase: 8 + min_entropy: 3.6 + confidence: medium + visible: false + examples: + - POLY_BUILDER_SECRET=QmFzZTY0U2VjcmV0VGVzdEtleTEyMzQ1Njc4OTBBQkNERUY= + - 'polymarket_builder_secret: Q29tcGxleFNlY3JldE1hdGVyaWFsMTIzNDU2Nzg5MDEyMzQ=' + references: + - https://docs.polymarket.com/trading/orders/attribution + + - name: Polymarket Builder Passphrase + id: kingfisher.polymarket.2 + pattern: | + (?xi) + \b + poly(?:market)? + (?:.|[\n\r]){0,32}? + (?:builder|api)? + (?:.|[\n\r]){0,16}? + passphrase + (?:.|[\n\r]){0,12}? + ( + [A-Za-z0-9_]{8,128} + ) + \b + pattern_requirements: + min_digits: 1 + min_uppercase: 1 + min_lowercase: 4 + ignore_if_contains: + - example + - placeholder + min_entropy: 3.0 + confidence: medium + visible: false + examples: + - POLY_BUILDER_PASSPHRASE=BuilderPass_2026 + - 'polymarket_passphrase: AlphaPass_7788' + references: + - https://docs.polymarket.com/trading/orders/attribution + + - name: Polymarket Builder API Key + id: kingfisher.polymarket.3 + pattern: | + (?xi) + \b + poly(?:market)? + (?:.|[\n\r]){0,32}? + (?:builder|api)? + (?:.|[\n\r]){0,16}? + key + (?:.|[\n\r]){0,12}? + ( + [a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12} + ) + \b + pattern_requirements: + min_digits: 6 + min_lowercase: 8 + min_entropy: 3.2 + confidence: medium + examples: + - POLY_BUILDER_API_KEY=12345678-abcd-1234-efab-1234567890ab + - 'polymarket_api_key: a1b2c3d4-e5f6-789a-bcde-f0123456789a' + references: + - https://docs.polymarket.com/trading/orders/attribution diff --git a/crates/kingfisher-rules/data/rules/privateai.yml b/crates/kingfisher-rules/data/rules/privateai.yml new file mode 100644 index 0000000..209fb7e --- /dev/null +++ b/crates/kingfisher-rules/data/rules/privateai.yml @@ -0,0 +1,58 @@ +rules: + - name: Private AI API Key + id: kingfisher.privateai.1 + pattern: | + (?xi) + \b + (?: + private[_-]?ai + | + limina + ) + (?:.|[\n\r]){0,32}? + (?: + api[_-]?key + | + x-api-key + | + token + ) + (?:.|[\n\r]){0,12}? + ( + [a-z0-9]{32} + ) + \b + pattern_requirements: + min_digits: 4 + min_lowercase: 8 + ignore_if_contains: + - example + - placeholder + - insert + - your + min_entropy: 3.5 + confidence: medium + examples: + - PRIVATEAI_API_KEY=4fa2d7c81be9063d4ea8bc1f6d2a7e9c + - 'privateai_x_api_key: 2ab4d6e8f0c1a3b5d7e9f1a2b4c6d8e0' + references: + - https://docs.private-ai.com/fundamentals/getting-started + - https://docs.private-ai.com/reference/4.0.0/operation/ner_text_ner_text_post/ + validation: + type: Http + content: + request: + method: POST + url: https://api.private-ai.com/community/v4/process/text + headers: + Content-Type: application/json + x-api-key: '{{ TOKEN }}' + body: '{"text":["Hello Jane Doe"]}' + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: JsonValid + - type: WordMatch + words: + - '"processed_text"' diff --git a/crates/kingfisher-rules/data/rules/settlemint.yml b/crates/kingfisher-rules/data/rules/settlemint.yml new file mode 100644 index 0000000..2c38e29 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/settlemint.yml @@ -0,0 +1,63 @@ +rules: + - name: SettleMint Personal Access Token + id: kingfisher.settlemint.1 + pattern: | + (?x) + \b + ( + sm_pat_[A-Za-z0-9]{16} + ) + \b + pattern_requirements: + min_digits: 2 + min_uppercase: 1 + min_lowercase: 4 + min_entropy: 3.2 + confidence: medium + examples: + - SETTLEMINT_ACCESS_TOKEN=sm_pat_A1b2C3d4E5f6G7h8 + - settlemint connect --pat=sm_pat_Z9y8X7w6V5u4T3s2 + references: + - https://console.settlemint.com/documentation/blockchain-platform/platform-components/security-and-authentication/personal-access-tokens + + - name: SettleMint Application Access Token + id: kingfisher.settlemint.2 + pattern: | + (?x) + \b + ( + sm_aat_[A-Za-z0-9]{16} + ) + \b + pattern_requirements: + min_digits: 2 + min_uppercase: 1 + min_lowercase: 4 + min_entropy: 3.2 + confidence: medium + examples: + - BLOCKSCOUT_SETTLEMINT_APPLICATION_ACCESS_TOKEN=sm_aat_A1b2C3d4E5f6G7h8 + - 'x-auth-token: sm_aat_Z9y8X7w6V5u4T3s2' + references: + - https://console.settlemint.com/documentation/blockchain-platform/platform-components/security-and-authentication/application-access-tokens + + - name: SettleMint Service Access Token + id: kingfisher.settlemint.3 + pattern: | + (?x) + \b + ( + sm_sat_[A-Za-z0-9]{16} + ) + \b + pattern_requirements: + min_digits: 2 + min_uppercase: 1 + min_lowercase: 4 + min_entropy: 3.2 + confidence: medium + examples: + - SETTLEMINT_SERVICE_TOKEN=sm_sat_A1b2C3d4E5f6G7h8 + - 'Authorization: Bearer sm_sat_Z9y8X7w6V5u4T3s2' + references: + - https://console.settlemint.com/documentation diff --git a/crates/kingfisher-rules/data/rules/sidekiq.yml b/crates/kingfisher-rules/data/rules/sidekiq.yml new file mode 100644 index 0000000..0a982dc --- /dev/null +++ b/crates/kingfisher-rules/data/rules/sidekiq.yml @@ -0,0 +1,58 @@ +rules: + - name: Sidekiq Enterprise Credential + id: kingfisher.sidekiq.1 + pattern: | + (?xi) + \b + (?: + BUNDLE_ENTERPRISE__CONTRIBSYS__COM + | + BUNDLE_GEMS__CONTRIBSYS__COM + ) + \s*[:=]\s* + ["']? + ( + [a-f0-9]{8}:[a-f0-9]{8} + ) + ["']? + \b + pattern_requirements: + min_digits: 4 + min_lowercase: 4 + min_entropy: 2.8 + confidence: medium + examples: + - BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafe1234:dead5678 + - 'export BUNDLE_GEMS__CONTRIBSYS__COM="ca1eb4b3:d3ad533f"' + + - name: Sidekiq Sensitive URL + id: kingfisher.sidekiq.2 + pattern: | + (?xi) + ( + https?:// + [a-f0-9]{8}:[a-f0-9]{8} + @ + (?: + gems\.contribsys\.com + | + enterprise\.contribsys\.com + ) + (?: + /[^ \t\r\n"'<>]* + | + \?[^ \t\r\n"'<>]* + | + \#[^ \t\r\n"'<>]* + | + :[0-9]{1,5}(?:/[^ \t\r\n"'<>]*)? + )? + ) + pattern_requirements: + min_digits: 4 + min_lowercase: 4 + min_entropy: 2.8 + confidence: medium + examples: + - https://cafe1234:dead5678@gems.contribsys.com/ + - http://ca1eb4b3:d3ad533f@enterprise.contribsys.com:80/path?param1=true diff --git a/crates/kingfisher-rules/data/rules/stytch.yml b/crates/kingfisher-rules/data/rules/stytch.yml new file mode 100644 index 0000000..1fe26cb --- /dev/null +++ b/crates/kingfisher-rules/data/rules/stytch.yml @@ -0,0 +1,76 @@ +rules: + - name: Stytch Project ID + id: kingfisher.stytch.1 + visible: false + pattern: | + (?xi) + \b + ( + project-(?:test|live)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} + ) + \b + min_entropy: 2.0 + confidence: medium + examples: + - project-test-8aed2e54-0266-4793-9b5e-0cc9c56064da + references: + - https://stytch.com/docs/api-reference/consumer/api/overview + - https://stytch.com/docs/api-reference/b2b/api/sessions/authenticate-jwt + + - name: Stytch Project Secret + id: kingfisher.stytch.2 + pattern: | + (?xi) + \b + ( + secret-(?:test|live)-[A-Za-z0-9_-]{35}=? + ) + (?:[^A-Za-z0-9_=-]|$) + pattern_requirements: + min_digits: 2 + min_uppercase: 1 + min_lowercase: 1 + min_entropy: 3.5 + confidence: medium + examples: + - secret-test-IJ7zLTgXp8xoS7yXO2xavNxZTbYfvm-2nZM= + references: + - https://stytch.com/docs/api-reference/consumer/api/overview + - https://stytch.com/docs/api-reference/b2b/api/m2m/overview + - https://stytch.com/docs/api-reference/b2b/api/sessions/authenticate-jwt + depends_on_rule: + - rule_id: kingfisher.stytch.1 + variable: STYTCH_PROJECT_ID + validation: + type: Http + content: + request: + method: POST + url: > + {%- if TOKEN contains "-live-" -%} + https://api.stytch.com/v1/m2m/clients/search + {%- else -%} + https://test.stytch.com/v1/m2m/clients/search + {%- endif -%} + headers: + Authorization: "Basic {{ STYTCH_PROJECT_ID | append: ':' | append: TOKEN | b64enc }}" + Content-Type: application/json + body: | + { + "query": { + "operator": "AND", + "operands": [ + { + "filter_name": "status", + "filter_value": ["active"] + } + ] + } + } + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: WordMatch + words: + - '"m2m_clients"' diff --git a/crates/kingfisher-rules/data/rules/trello.yml b/crates/kingfisher-rules/data/rules/trello.yml new file mode 100644 index 0000000..fbe8821 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/trello.yml @@ -0,0 +1,31 @@ +rules: + - name: Trello API Token + id: kingfisher.trello.1 + pattern: | + (?xi) + \b + trello + (?:.|[\n\r]){0,32}? + (?: + token | + api[_-]?token | + access[_-]?token + ) + (?:.|[\n\r]){0,12}? + ( + [A-Za-z0-9]{32} + ) + \b + pattern_requirements: + min_digits: 2 + min_lowercase: 6 + ignore_if_contains: + - yourtoken + - placeholder + min_entropy: 3.1 + confidence: medium + examples: + - TRELLO_TOKEN=0a1b2c3d4e5f6g7h8i9j0k1l2m3n4p5q + - trello_access_token="Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56" + references: + - https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/ diff --git a/crates/kingfisher-rules/data/rules/zapier.yml b/crates/kingfisher-rules/data/rules/zapier.yml new file mode 100644 index 0000000..83f37f2 --- /dev/null +++ b/crates/kingfisher-rules/data/rules/zapier.yml @@ -0,0 +1,20 @@ +rules: + - name: Zapier Webhook URL + id: kingfisher.zapier.1 + pattern: | + (?x) + \b + ( + https://hooks\.zapier\.com/hooks/catch/ + [0-9]{5,10} + / + [a-z0-9]{5,12} + /? + ) + min_entropy: 3.4 + confidence: medium + examples: + - ZAPIER_WEBHOOK=https://hooks.zapier.com/hooks/catch/11595998/3ouwv7m/ + - webhook_url="https://hooks.zapier.com/hooks/catch/2929690/ztd17n/" + references: + - https://help.zapier.com/hc/en-us/articles/8496288690317-Trigger-Zaps-from-webhooks diff --git a/crates/kingfisher-rules/data/rules/zendesk.yml b/crates/kingfisher-rules/data/rules/zendesk.yml new file mode 100644 index 0000000..e4fa89a --- /dev/null +++ b/crates/kingfisher-rules/data/rules/zendesk.yml @@ -0,0 +1,105 @@ +rules: + - name: Zendesk Subdomain + id: kingfisher.zendesk.1 + pattern: | + (?xi) + \b + ( + [a-z0-9] + (?: + [a-z0-9-]{0,61} + [a-z0-9] + )? + \.zendesk\.com + ) + \b + min_entropy: 2.0 + confidence: medium + visible: false + examples: + - acme-support.zendesk.com + - helpdesk-prod.zendesk.com + references: + - https://developer.zendesk.com/api-reference/introduction/doc-conventions/ + + - name: Zendesk Account Email + id: kingfisher.zendesk.2 + pattern: | + (?xi) + \b + (?:zendesk|zd) + (?:.|[\n\r]){0,32}? + (?:email|user(?:name)?) + (?:.|[\n\r]){0,12}? + ( + [A-Za-z0-9._%+\-]+ + @ + [A-Za-z0-9.\-]+\.[A-Za-z]{2,} + ) + \b + min_entropy: 2.0 + confidence: medium + visible: false + examples: + - ZENDESK_EMAIL=agent@example.com + - 'zendesk_user: "support.bot@example.org"' + references: + - https://developer.zendesk.com/api-reference/introduction/security-and-auth/ + + - name: Zendesk API Token + id: kingfisher.zendesk.3 + pattern: | + (?xi) + \b + (?:zendesk|zd) + (?:.|[\n\r]){0,48}? + (?: + api[\s_.-]*token + | + token + | + api[\s_.-]*key + ) + (?:.|[\n\r]){0,16}? + ( + [A-Za-z0-9]{40} + ) + \b + pattern_requirements: + min_digits: 2 + min_uppercase: 1 + min_lowercase: 1 + ignore_if_contains: + - example + - placeholder + - yourtoken + min_entropy: 3.8 + confidence: medium + examples: + - ZENDESK_API_TOKEN=a3B8f29E4d1C6a0578e23D9f41b6C8e2qR7tY4uI + - zendesk_token="E7d2A1f849c3B05d6e81F2a794c3D5b0pQ8wX1zK" + references: + - https://developer.zendesk.com/api-reference/introduction/security-and-auth/ + - https://developer.zendesk.com/api-reference/ticketing/account-configuration/current_user/ + depends_on_rule: + - rule_id: kingfisher.zendesk.1 + variable: ZENDESK_HOST + - rule_id: kingfisher.zendesk.2 + variable: ZENDESK_EMAIL + validation: + type: Http + content: + request: + method: GET + url: 'https://{{ ZENDESK_HOST }}/api/v2/users/me.json' + headers: + Accept: application/json + Authorization: 'Basic {{ ZENDESK_EMAIL | append: "/token:" | append: TOKEN | b64enc }}' + response_matcher: + - report_response: true + - type: StatusMatch + status: [200] + - type: JsonValid + - type: WordMatch + words: + - '"user"' diff --git a/crates/kingfisher-rules/src/liquid_filters.rs b/crates/kingfisher-rules/src/liquid_filters.rs index 16a8e8b..77fba6e 100644 --- a/crates/kingfisher-rules/src/liquid_filters.rs +++ b/crates/kingfisher-rules/src/liquid_filters.rs @@ -182,6 +182,45 @@ impl Filter for HmacSha256Filter { } } +// ── HMAC-SHA256 with base64-encoded key ────────────────────────────────── +#[derive(Debug, FilterParameters)] +struct HmacB64KeyArgs { + #[parameter(description = "Base64-encoded HMAC key", arg_type = "str")] + key: Expression, +} + +#[derive(Clone, ParseFilter, FilterReflection, Default)] +#[filter( + name = "hmac_sha256_b64key", + description = "HMAC-SHA256 with a base64-encoded key – decodes the key to raw bytes before signing. Returns Base64.", + parameters(HmacB64KeyArgs), + parsed(HmacSha256B64KeyFilter) +)] +pub struct HmacSha256B64Key; + +#[derive(Debug, FromFilterParameters, Display_filter)] +#[name = "hmac_sha256_b64key"] +struct HmacSha256B64KeyFilter { + #[parameters] + args: HmacB64KeyArgs, +} + +impl Filter for HmacSha256B64KeyFilter { + fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result { + let args = self.args.evaluate(runtime)?; + let key_b64 = args.key.to_kstr(); + + let key_bytes = general_purpose::STANDARD.decode(key_b64.as_bytes()).map_err(|e| { + LiquidError::with_msg(format!("hmac_sha256_b64key: invalid base64 key: {e}")) + })?; + + let mut mac = Hmac::::new_from_slice(&key_bytes) + .map_err(|e| LiquidError::with_msg(format!("hmac_sha256_b64key: {e}")))?; + mac.update(input.to_kstr().as_bytes()); + Ok(Value::scalar(general_purpose::STANDARD.encode(mac.finalize().into_bytes()))) + } +} + // ── HMAC-SHA1 ───────────────────────────────────────────── #[derive(Debug, FilterParameters)] struct HmacSha1Args { @@ -439,6 +478,14 @@ impl Filter for B64DecFilter { } } +// {{ "any" | newline }} → "\n" (appends nothing, just returns a newline character) +static_filter!( + /// Returns a single newline character. Useful inside YAML block scalars where + /// a literal newline in the template would break indentation. + NewlineFilter, "newline", + |_input: &dyn ValueView| -> String { "\n".to_string() } +); + // ----------------------------------------------------------------------------- // Authentication & Security // ----------------------------------------------------------------------------- @@ -912,6 +959,7 @@ pub fn register_all(builder: liquid::ParserBuilder) -> liquid::ParserBuilder { .filter(JwtHeaderFilter::default()) .filter(B64EncFilter::default()) .filter(B64DecFilter::default()) + .filter(NewlineFilter::default()) .filter(RandomStringFilter::default()) .filter(SuffixFilter::default()) .filter(PrefixFilter::default()) @@ -923,6 +971,7 @@ pub fn register_all(builder: liquid::ParserBuilder) -> liquid::ParserBuilder { .filter(Base62Filter::default()) .filter(Base36Filter::default()) .filter(HmacSha256::default()) + .filter(HmacSha256B64Key::default()) .filter(HmacSha1::default()) .filter(HmacSha384::default()) } @@ -1073,6 +1122,21 @@ mod tests { assert_eq!(render(r#"{{ "hi!" | hmac_sha256: "secret" }}"#), expect); } + #[test] + fn hmac_sha256_b64key_filter() { + // Key is base64-encoded; the filter must decode it to raw bytes before HMAC. + let raw_key: &[u8] = &[0x00, 0x80, 0xFF, 0x42, 0xDE, 0xAD, 0xBE, 0xEF]; + let b64_key = general_purpose::STANDARD.encode(raw_key); + + let data = b"hello azure"; + let mut mac = Hmac::::new_from_slice(raw_key).unwrap(); + mac.update(data); + let expect = general_purpose::STANDARD.encode(mac.finalize().into_bytes()); + + let template = format!(r#"{{{{ "hello azure" | hmac_sha256_b64key: "{b64_key}" }}}}"#); + assert_eq!(render(&template), expect); + } + #[test] fn hmac_sha384_filter() { let key = b"topsecret"; diff --git a/docs/LIBRARY.md b/docs/LIBRARY.md index 313af28..b4aa0bb 100644 --- a/docs/LIBRARY.md +++ b/docs/LIBRARY.md @@ -315,10 +315,10 @@ let template = parser.parse("{{ secret | sha256 }}")?; Available filters: - **Encoding**: `b64enc`, `b64dec`, `b64url_enc`, `url_encode`, `json_escape` -- **Hashing**: `sha256`, `crc32`, `crc32_dec`, `crc32_hex` -- **HMAC**: `hmac_sha256`, `hmac_sha384`, `hmac_sha1` +- **Hashing**: `sha256`, `crc32`, `crc32_dec`, `crc32_hex`, `crc32_le_b64` +- **HMAC**: `hmac_sha256`, `hmac_sha384`, `hmac_sha1`, `hmac_sha256_b64key` - **Encoding**: `base62`, `base36` -- **Strings**: `prefix`, `suffix`, `replace`, `lstrip_chars`, `random_string` +- **Strings**: `prefix`, `suffix`, `replace`, `lstrip_chars`, `random_string`, `newline` - **Time**: `unix_timestamp`, `iso_timestamp`, `iso_timestamp_no_frac` - **Other**: `uuid`, `jwt_header` diff --git a/docs/RULES.md b/docs/RULES.md index c806aa2..5fe9bad 100644 --- a/docs/RULES.md +++ b/docs/RULES.md @@ -468,6 +468,7 @@ Below is the complete list of Liquid filters available in Kingfisher, along with | `hmac_sha1` | `key` (string) | Computes HMAC-SHA1 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha1: "secret-key" }}` | | `hmac_sha256` | `key` (string) | Computes HMAC-SHA256 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha256: "secret-key" }}` | | `hmac_sha384` | `key` (string) | Computes HMAC-SHA384 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha384: "secret-key" }}` | +| `hmac_sha256_b64key` | `key` (string, base64-encoded) | Decodes the key from Base64 to raw bytes, then computes HMAC-SHA256. Returns Base64. Use for Azure SAS and other protocols where the signing key is base64-encoded. | `{{ to_sign \| hmac_sha256_b64key: TOKEN }}` | | `random_string` | `len` (integer, optional) | Generates a cryptographically-secure random alphanumeric string of the specified length (default: 32). | `{{ "" \| random_string: 16 }}` | | `prefix` | `len` (integer, optional) | Returns the first `len` characters from the string (default: full). | `{{ TOKEN \| prefix: 6 }}` | | `suffix` | `len` (integer, optional) | Returns the last `len` characters from the string (default: full). | `{{ TOKEN \| suffix: 6 }}` | @@ -480,6 +481,8 @@ Below is the complete list of Liquid filters available in Kingfisher, along with | `uuid` | – | Generates a random UUIDv4 string. | `{{ "" \| uuid }}` | | `jwt_header` | – | Builds a minimal JWT header JSON (`{"typ":"JWT","alg":…}`) and Base64URL-encodes it. | `{{ "HS256" \| jwt_header }}` | | `replace` | `from` (string), `to` (string) | Replaces every occurrence of `from` with `to` in the input string. | `{{ "hello world" \| replace: "world", "mars" }}` | +| `newline` | – | Returns a single newline character (`\n`). Useful inside YAML block scalars where a literal newline would break indentation. | `{{ "" \| newline }}` | +| `base36` | `width` (integer, optional) | Encodes the input number as Base36, left-padding with zeros as needed. | `{{ TOKEN \| crc32 \| base36: 6 }}` | **Chaining & Composition:** Filters can be stacked; e.g.: