diff --git a/README.md b/README.md index 2cd2d93..492837d 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Kingfisher is an open source secret scanner and **live secret validation** tool built in Rust. -It combines Intel's SIMD-accelerated regex engine (Hyperscan) with language-aware parsing to achieve high accuracy at massive scale, and **ships with 938 built-in rules** (484 with live validation) to detect, **validate**, and triage leaked API keys, tokens, and credentials before they ever reach production. +It combines Intel's SIMD-accelerated regex engine (Hyperscan) with language-aware parsing to achieve high accuracy at massive scale, and ships with [938 built-in rules](https://mongodb.github.io/kingfisher/rules/builtin-rules/) to detect, **validate**, and triage leaked API keys, tokens, and credentials before they ever reach production. -Kingfisher also ships a **browser-based report viewer** that visualizes and triages findings from Kingfisher **and** from Gitleaks and TruffleHog JSON reports — so you can import scans from other tools and triage them in the same UI. A [hosted, upload-based copy of the viewer](https://mongodb.github.io/kingfisher/viewer/) is published on the Kingfisher docs site. +Kingfisher also ships a **browser-based report viewer** that visualizes and triages findings from Kingfisher **and** from Gitleaks and TruffleHog JSON reports — so you can import scans from other tools and triage them in the same UI. A [hosted copy of the viewer](https://mongodb.github.io/kingfisher/viewer/) is published on the Kingfisher docs site. Designed for offensive security engineers and blue-team defenders alike, Kingfisher helps you scan repositories, cloud storage, chat, docs, and CI pipelines to find and verify exposed secrets quickly. @@ -82,50 +82,6 @@ kingfisher scan /path/to/scan --view-report NOTE: Replay has been slowed down for demo ![Kingfisher secret scanning demo](docs/kingfisher-usage-01.gif) -## Report Viewer (local and hosted) - -Kingfisher ships a browser-based **report viewer and triager** for three formats: - -- Kingfisher JSON / JSONL (with full `access_map` blast-radius data when present) -- **Gitleaks** JSON -- **TruffleHog** JSON / JSONL (verified findings are surfaced as active credentials) - -There are two ways to use it: - -1. **Locally via the CLI** — `kingfisher view ./report.json` (bundled into every Kingfisher binary; no external services) -2. **Hosted** — [https://mongodb.github.io/kingfisher/viewer/](https://mongodb.github.io/kingfisher/viewer/) — a static, client-side upload-based copy of the same viewer. Drag in Kingfisher, Gitleaks, or TruffleHog reports and triage in your browser; nothing is uploaded to a server. - -### Why use a visual viewer / triager? - -Raw JSON from Kingfisher, Gitleaks, or TruffleHog is great for machines, but awful for humans making decisions on which findings are real and which need to be rotated first. The viewer lets a security engineer: - -- **Skim hundreds of findings at a glance**, grouped by detector, file, repository, and validation status instead of one line per finding in a terminal. -- **Triage across multiple tools in one place** — import a Gitleaks report plus a TruffleHog report plus a Kingfisher scan of the same repo and look at them side-by-side with dedup, instead of eyeballing three different JSON schemas. -- **Prioritize real, validated secrets** — validated Kingfisher findings and TruffleHog-verified findings float to the top so you act on live credentials first. -- **Drop duplicates** — repeated imports and overlapping scans are deduplicated by fingerprint/secret identity so you don't open the same key five times. Per-tool "duplicates removed" cards on the dashboard show how much noise each tool contributed, and an upload-time **Deduplicate findings** toggle (on by default) lets you inspect raw rows when you need to. -- **Cross-tool enrichment** — when a Gitleaks or TruffleHog finding lines up with a Kingfisher finding at the same commit, file, and line, the imported row picks up Kingfisher's validation verdict and validate / revoke commands. This is useful when a team already has a Gitleaks or TruffleHog pipeline in CI and wants to layer Kingfisher's validation and remediation data on top of the reports they already produce, without replacing their existing tooling. -- **See blast radius** — for Kingfisher reports generated with `--access-map`, the viewer renders the identity, permissions, and resources a leaked credential can reach, so you can tell a dev token apart from a production admin key. -- **Export triage decisions** — filter down to what matters and export a cleaned-up subset for a ticket, a rotation runbook, or an audit reviewer. - -Gitleaks and TruffleHog are both widely used open-source secret scanners with their own strengths; Kingfisher's viewer reads their standard JSON output so teams that already run them can pull those findings into the same triage workflow. Kingfisher is not affiliated with or endorsed by the Gitleaks project or Truffle Security Co.; TruffleHog and Gitleaks are trademarks of their respective owners. - -Note: when you pass `--view-report`, Kingfisher starts a web server on port `7890` (default) and opens it in your default browser. By default it binds to `127.0.0.1` for security. You'll see this near the end of the scan output, and **Kingfisher will keep running** until you stop it. - -```bash -INFO kingfisher::cli::commands::view: Starting access-map viewer address=127.0.0.1:7890 -Serving access-map viewer at http://127.0.0.1:7890 (Ctrl+C to stop) -``` - -**Usage:** -```bash -kingfisher scan /path/to/scan --access-map --view-report -``` - -![Kingfisher access map and report viewer demo](docs/kingfisher-usage-access-map-01.gif) - -**Click to view video** -[![Demo](docs/demos/findings-thumbnail.png)](https://github.com/user-attachments/assets/d33ee7a6-c60a-4e42-88e0-ac03cb429a46) - # Table of Contents - [What Is Kingfisher?](#what-is-kingfisher) @@ -391,6 +347,51 @@ gh release download --repo mongodb/kingfisher \ gh attestation verify kingfisher-linux-x64.tgz --repo mongodb/kingfisher ``` + +## Report Viewer (local and hosted) + +Kingfisher ships a browser-based **report viewer and triager** for three formats: + +- Kingfisher JSON / JSONL (with full `access_map` blast-radius data when present) +- **Gitleaks** JSON +- **TruffleHog** JSON / JSONL (verified findings are surfaced as active credentials) + +There are two ways to use it: + +1. **Locally via the CLI** — `kingfisher view ./report.json` (bundled into every Kingfisher binary; no external services) +2. **Hosted** — [https://mongodb.github.io/kingfisher/viewer/](https://mongodb.github.io/kingfisher/viewer/) — a static, client-side upload-based copy of the same viewer. Drag in Kingfisher, Gitleaks, or TruffleHog reports and triage in your browser; nothing is uploaded to a server. + +### Why use a visual viewer / triager? + +Raw JSON from Kingfisher, Gitleaks, or TruffleHog is great for machines, but awful for humans making decisions on which findings are real and which need to be rotated first. The viewer lets a security engineer: + +- **Skim hundreds of findings at a glance**, grouped by detector, file, repository, and validation status instead of one line per finding in a terminal. +- **Triage across multiple tools in one place** — import a Gitleaks report plus a TruffleHog report plus a Kingfisher scan of the same repo and look at them side-by-side with dedup, instead of eyeballing three different JSON schemas. +- **Prioritize real, validated secrets** — validated Kingfisher findings and TruffleHog-verified findings float to the top so you act on live credentials first. +- **Drop duplicates** — repeated imports and overlapping scans are deduplicated by fingerprint/secret identity so you don't open the same key five times. Per-tool "duplicates removed" cards on the dashboard show how much noise each tool contributed, and an upload-time **Deduplicate findings** toggle (on by default) lets you inspect raw rows when you need to. +- **Cross-tool enrichment** — when a Gitleaks or TruffleHog finding lines up with a Kingfisher finding at the same commit, file, and line, the imported row picks up Kingfisher's validation verdict and validate / revoke commands. This is useful when a team already has a Gitleaks or TruffleHog pipeline in CI and wants to layer Kingfisher's validation and remediation data on top of the reports they already produce, without replacing their existing tooling. +- **See blast radius** — for Kingfisher reports generated with `--access-map`, the viewer renders the identity, permissions, and resources a leaked credential can reach, so you can tell a dev token apart from a production admin key. +- **Export triage decisions** — filter down to what matters and export a cleaned-up subset for a ticket, a rotation runbook, or an audit reviewer. + +Gitleaks and TruffleHog are both widely used open-source secret scanners with their own strengths; Kingfisher's viewer reads their standard JSON output so teams that already run them can pull those findings into the same triage workflow. Kingfisher is not affiliated with or endorsed by the Gitleaks project or Truffle Security Co.; TruffleHog and Gitleaks are trademarks of their respective owners. + +Note: when you pass `--view-report`, Kingfisher starts a web server on port `7890` (default) and opens it in your default browser. By default it binds to `127.0.0.1` for security. You'll see this near the end of the scan output, and **Kingfisher will keep running** until you stop it. + +```bash +INFO kingfisher::cli::commands::view: Starting access-map viewer address=127.0.0.1:7890 +Serving access-map viewer at http://127.0.0.1:7890 (Ctrl+C to stop) +``` + +**Usage:** +```bash +kingfisher scan /path/to/scan --access-map --view-report +``` + +![Kingfisher access map and report viewer demo](docs/kingfisher-usage-access-map-01.gif) + +**Click to view video** +[![Demo](docs/demos/findings-thumbnail.png)](https://github.com/user-attachments/assets/d33ee7a6-c60a-4e42-88e0-ac03cb429a46) + # Detection Rules Kingfisher ships with [938 built-in rules](crates/kingfisher-rules/data/rules/) covering cloud keys, AI tokens, CI/CD secrets, database credentials, and SaaS API keys. Below is an overview — see the full list in [crates/kingfisher-rules/data/rules/](crates/kingfisher-rules/data/rules/): diff --git a/crates/kingfisher-rules/data/rules/agora.yml b/crates/kingfisher-rules/data/rules/agora.yml index 2a328dc..fbf7168 100644 --- a/crates/kingfisher-rules/data/rules/agora.yml +++ b/crates/kingfisher-rules/data/rules/agora.yml @@ -17,7 +17,7 @@ rules: pattern_requirements: min_digits: 4 min_entropy: 3.0 - confidence: low + confidence: medium visible: false examples: - AGORA_APP_ID=78b8de76d5678a6feb2605721c0aefbe diff --git a/crates/kingfisher-rules/data/rules/cypress.yml b/crates/kingfisher-rules/data/rules/cypress.yml index 329b5bd..e739982 100644 --- a/crates/kingfisher-rules/data/rules/cypress.yml +++ b/crates/kingfisher-rules/data/rules/cypress.yml @@ -57,4 +57,4 @@ rules: min_entropy: 1.0 examples: - 'CYPRESS_PROJECT_ID=a7bq2k' - - 'projectId: "abc123"' + - 'CYPRESS_PROJECT_ID: "abc123"' diff --git a/crates/kingfisher-rules/data/rules/highnote.yml b/crates/kingfisher-rules/data/rules/highnote.yml index 846ac99..3c5cfac 100644 --- a/crates/kingfisher-rules/data/rules/highnote.yml +++ b/crates/kingfisher-rules/data/rules/highnote.yml @@ -17,8 +17,7 @@ rules: confidence: medium categories: [api, key] examples: - - 'HIGHNOTE_API_KEY=sk_live_AbCdEfGhIjKlMnOpQrStUvWxYz1234' - - 'highnote_key: rk_test_AbCdEfGhIjKlMnOpQrStUvWxYz1234' + - 'HIGHNOTE_API_KEY="sk_live_a2V5XzAxS1BSWE1LTjBEWE1INlpBU0VEWjU2VFE3LFdjOWxFMTNDS29xRkdlYU9uMUpDbUpTZWE"' validation: type: Http content: diff --git a/crates/kingfisher-rules/data/rules/huawei.yml b/crates/kingfisher-rules/data/rules/huawei.yml index 7f1e74c..693f593 100644 --- a/crates/kingfisher-rules/data/rules/huawei.yml +++ b/crates/kingfisher-rules/data/rules/huawei.yml @@ -17,7 +17,7 @@ rules: pattern_requirements: min_digits: 6 min_entropy: 2.0 - confidence: low + confidence: medium visible: false examples: - "{ \"Huawei\": { \"ClientId\": \"100809947\", \"ClientSecret\": \"...\" } }" diff --git a/crates/kingfisher-rules/data/rules/onfido.yml b/crates/kingfisher-rules/data/rules/onfido.yml index 2d2c2b1..6f84055 100644 --- a/crates/kingfisher-rules/data/rules/onfido.yml +++ b/crates/kingfisher-rules/data/rules/onfido.yml @@ -5,7 +5,7 @@ rules: (?x) \b ( - api_live\.[a-zA-Z0-9_-]{20,80} + api_(?:live|sandbox)\.[a-zA-Z0-9_-]{20,80} ) \b pattern_requirements: diff --git a/crates/kingfisher-rules/data/rules/paddle.yml b/crates/kingfisher-rules/data/rules/paddle.yml index 1102874..eda8fee 100644 --- a/crates/kingfisher-rules/data/rules/paddle.yml +++ b/crates/kingfisher-rules/data/rules/paddle.yml @@ -21,7 +21,6 @@ rules: categories: [api, key] examples: - 'PADDLE_API_KEY=pdl_live_apikey_01kps076233qscw38dxz320d0e_Ab3D5fGb7Jk9LmNpQrStUv_X2z' - - 'paddle_key: pdl_sdbx_apikey_01kps08npwb1rv4ryqxh0jr30c_qwErTyUiObAsDfGhJkLmN2_B7Q' validation: type: Http content: diff --git a/crates/kingfisher-rules/data/rules/stripe.yml b/crates/kingfisher-rules/data/rules/stripe.yml index c9fafc7..08cca81 100644 --- a/crates/kingfisher-rules/data/rules/stripe.yml +++ b/crates/kingfisher-rules/data/rules/stripe.yml @@ -36,7 +36,7 @@ rules: confidence: medium examples: - stripe_secret_key = sk_live_f01c79xuuug7yodgzj5ws0h1x2kyvho3 - - stripe_secret_key = pk_live_51O4GlNLQpd8Ph8H3or6Sv8fhuSPIQncX0dY318y8Hc9SYRyS4aeyrTN19ztOmAsuVZSTKNfI7RZoSOwNkLa0cwm010oLA68VFA + - stripe_secret_key = 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 - "strp_sec_key: rk_live_4haG9YwGkL2hXqTj5pSzo8FzB3uCwE7n" validation: type: Http diff --git a/crates/kingfisher-rules/data/rules/volcengine.yml b/crates/kingfisher-rules/data/rules/volcengine.yml index b3fc7f0..6056c57 100644 --- a/crates/kingfisher-rules/data/rules/volcengine.yml +++ b/crates/kingfisher-rules/data/rules/volcengine.yml @@ -16,6 +16,6 @@ rules: min_entropy: 3.0 confidence: medium examples: - - 'VOLCENGINE_ACCESS_KEY=AKLTY2IwOGJIMTdiZmI5NGU1MWFiNWE3MWJkNWY2MDdmOGU' + - 'VOLCENGINE_ACCESS_KEY=AKLTY2IwOGJIMTdiZmI5NGU1MWFiNWE3MWJkNWY2MDdmOGUxYz' references: - https://www.volcengine.com/docs/6291/65568 \ No newline at end of file diff --git a/crates/kingfisher-rules/data/rules/webex.yml b/crates/kingfisher-rules/data/rules/webex.yml index 3cf82c4..f290df0 100644 --- a/crates/kingfisher-rules/data/rules/webex.yml +++ b/crates/kingfisher-rules/data/rules/webex.yml @@ -17,7 +17,7 @@ rules: pattern_requirements: min_digits: 4 min_entropy: 3.5 - confidence: low + confidence: medium visible: false examples: - "webex_client = Ac0769801df88a3535b4b018ef570b499002bda401b3b8789259a937f22d66095" diff --git a/crates/kingfisher-rules/data/rules/workos.yml b/crates/kingfisher-rules/data/rules/workos.yml index 0d89127..5602138 100644 --- a/crates/kingfisher-rules/data/rules/workos.yml +++ b/crates/kingfisher-rules/data/rules/workos.yml @@ -20,7 +20,6 @@ rules: confidence: medium examples: - 'WORKOS_API_KEY="sk_live_a2V5XzAxS1BSWE1LTjBEWE1INlpBU0VEWjU2VFE3LFdjOWxFMTNDS29xRkdlYU9uMUpDbUpTZWE"' - - 'workos apiKey: "sk_test_a2V5XzAxS1BSWE1LTjBEWE1INlpBU0VEWjU2VFE3LFdjOWxFMTNDS29xRkdlYU9uMUpDbUpTZWE"' references: - https://workos.com/docs/reference/api-keys - https://workos.com/docs/authkit/api-keys diff --git a/crates/kingfisher-rules/data/rules/xendit.yml b/crates/kingfisher-rules/data/rules/xendit.yml index 6a35876..723835c 100644 --- a/crates/kingfisher-rules/data/rules/xendit.yml +++ b/crates/kingfisher-rules/data/rules/xendit.yml @@ -26,6 +26,8 @@ rules: Accept: application/json response_matcher: - report_response: true + - type: StatusMatch + status: [200] - type: JsonValid - type: WordMatch words: diff --git a/docs-site/docs/assets/javascripts/rules-filter.js b/docs-site/docs/assets/javascripts/rules-filter.js index 76051d1..493418c 100644 --- a/docs-site/docs/assets/javascripts/rules-filter.js +++ b/docs-site/docs/assets/javascripts/rules-filter.js @@ -1,13 +1,19 @@ -// Client-side search/filter for the built-in rules table -document.addEventListener("DOMContentLoaded", function () { - const input = document.querySelector(".rules-search"); - if (!input) return; - +// Client-side search/filter for the built-in rules table. +// Material's `navigation.instant` feature swaps page bodies without firing +// DOMContentLoaded, so we subscribe to the `document$` observable it exposes +// and re-wire the handler every time a new page is rendered. +function initRulesFilter() { const table = document.querySelector(".rules-table"); if (!table) return; - const rows = Array.from(table.querySelectorAll("tbody tr")); + const input = document.querySelector(".rules-search"); const countEl = document.querySelector(".rules-count"); + const tbody = table.querySelector("tbody"); + + if (table.dataset.rulesFilterBound === "1") return; + table.dataset.rulesFilterBound = "1"; + + const rows = Array.from(tbody.querySelectorAll("tr")); const total = rows.length; function updateCount(visible) { @@ -16,23 +22,98 @@ document.addEventListener("DOMContentLoaded", function () { } } - let debounceTimer; - input.addEventListener("input", function () { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(function () { - const query = input.value.toLowerCase().trim(); - let visible = 0; + function applyFilter() { + if (!input) { + updateCount(total); + return; + } + const query = input.value.toLowerCase().trim(); + let visible = 0; + rows.forEach(function (row) { + const text = row.textContent.toLowerCase(); + const match = !query || text.indexOf(query) !== -1; + row.style.display = match ? "" : "none"; + if (match) visible++; + }); + updateCount(visible); + } - rows.forEach(function (row) { - const text = row.textContent.toLowerCase(); - const match = !query || text.indexOf(query) !== -1; - row.style.display = match ? "" : "none"; - if (match) visible++; - }); + if (input) { + let debounceTimer; + input.addEventListener("input", function () { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(applyFilter, 80); + }); + } - updateCount(visible); - }, 150); + // --- Sortable columns --- + const headers = Array.from(table.querySelectorAll("thead th")); + const confidenceOrder = { "high": 3, "medium": 2, "low": 1 }; + let sortState = { index: -1, dir: 1 }; + + function cellKey(row, index) { + const cell = row.children[index]; + if (!cell) return ""; + return cell.textContent.trim().toLowerCase(); + } + + function compare(a, b, index) { + const av = cellKey(a, index); + const bv = cellKey(b, index); + // Confidence column — ordered by severity + if (headers[index] && headers[index].textContent.trim().toLowerCase() === "confidence") { + return (confidenceOrder[av] || 0) - (confidenceOrder[bv] || 0); + } + // Yes/empty columns — Yes first + const aYes = av === "yes" ? 1 : 0; + const bYes = bv === "yes" ? 1 : 0; + if (aYes !== bYes) return bYes - aYes; + return av.localeCompare(bv, undefined, { numeric: true, sensitivity: "base" }); + } + + function sortBy(index) { + if (sortState.index === index) { + sortState.dir = -sortState.dir; + } else { + sortState.index = index; + sortState.dir = 1; + } + const dir = sortState.dir; + const sorted = rows.slice().sort(function (a, b) { + return dir * compare(a, b, index); + }); + const frag = document.createDocumentFragment(); + sorted.forEach(function (row) { frag.appendChild(row); }); + tbody.appendChild(frag); + + headers.forEach(function (h, i) { + h.classList.remove("is-sorted-asc", "is-sorted-desc"); + if (i === index) { + h.classList.add(dir === 1 ? "is-sorted-asc" : "is-sorted-desc"); + } + }); + } + + headers.forEach(function (th, i) { + th.classList.add("is-sortable"); + th.setAttribute("role", "button"); + th.setAttribute("tabindex", "0"); + th.addEventListener("click", function () { sortBy(i); }); + th.addEventListener("keydown", function (e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + sortBy(i); + } + }); }); updateCount(total); -}); +} + +if (typeof document$ !== "undefined" && document$.subscribe) { + document$.subscribe(initRulesFilter); +} else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initRulesFilter); +} else { + initRulesFilter(); +} diff --git a/docs-site/docs/assets/stylesheets/extra.css b/docs-site/docs/assets/stylesheets/extra.css index 2930eaf..7b1d355 100644 --- a/docs-site/docs/assets/stylesheets/extra.css +++ b/docs-site/docs/assets/stylesheets/extra.css @@ -265,6 +265,161 @@ margin-bottom: 1rem; } +.md-typeset table.rules-table, +table.rules-table { + display: table; + width: 100%; + table-layout: auto; + border-collapse: separate; + border-spacing: 0; + margin: 1rem 0 2rem; + font-size: 0.72rem; + line-height: 1.35; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 0.5rem; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); +} + +.md-typeset table.rules-table thead, +table.rules-table thead { + background: var(--md-primary-fg-color); +} + +.md-typeset table.rules-table thead th, +table.rules-table thead th { + color: #fff; + font-weight: 600; + text-align: left; + padding: 0.55rem 0.7rem; + border: none; + font-size: 0.7rem; + letter-spacing: 0.02em; + text-transform: uppercase; + white-space: nowrap; + position: sticky; + top: 0; + z-index: 1; +} + +.md-typeset table.rules-table thead th.is-sortable, +table.rules-table thead th.is-sortable { + cursor: pointer; + user-select: none; + padding-right: 1.75rem; + position: sticky; +} + +.md-typeset table.rules-table thead th.is-sortable::after, +table.rules-table thead th.is-sortable::after { + content: ""; + display: inline-block; + width: 0; + height: 0; + margin-left: 0.5rem; + vertical-align: middle; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid rgba(255, 255, 255, 0.35); + border-bottom: 5px solid rgba(255, 255, 255, 0.35); + opacity: 0.6; +} + +.md-typeset table.rules-table thead th.is-sortable:hover, +table.rules-table thead th.is-sortable:hover { + background: rgba(255, 255, 255, 0.08); +} + +.md-typeset table.rules-table thead th.is-sorted-asc::after, +table.rules-table thead th.is-sorted-asc::after { + border-top: 0; + border-bottom: 5px solid #fff; + opacity: 1; +} + +.md-typeset table.rules-table thead th.is-sorted-desc::after, +table.rules-table thead th.is-sorted-desc::after { + border-bottom: 0; + border-top: 5px solid #fff; + opacity: 1; +} + +.md-typeset table.rules-table tbody td, +table.rules-table tbody td { + padding: 0.4rem 0.7rem; + border: none; + border-bottom: 1px solid var(--md-default-fg-color--lightest); + vertical-align: middle; + background: var(--md-default-bg-color); + word-break: break-word; +} + +.md-typeset table.rules-table tbody tr:nth-child(even) td, +table.rules-table tbody tr:nth-child(even) td { + background: var(--md-code-bg-color); +} + +.md-typeset table.rules-table tbody tr:hover td, +table.rules-table tbody tr:hover td { + background: var(--md-accent-fg-color--transparent); +} + +.md-typeset table.rules-table tbody tr:last-child td, +table.rules-table tbody tr:last-child td { + border-bottom: none; +} + +.md-typeset table.rules-table td code, +table.rules-table td code { + background: var(--md-code-bg-color); + color: var(--md-code-fg-color); + padding: 0.1rem 0.35rem; + border-radius: 0.25rem; + font-size: 0.68rem; + white-space: nowrap; +} + +.md-typeset table.rules-table tbody tr:nth-child(even) td code, +table.rules-table tbody tr:nth-child(even) td code { + background: var(--md-default-bg-color); +} + +/* First column (Provider) — emphasized */ +.md-typeset table.rules-table tbody td:first-child, +table.rules-table tbody td:first-child { + font-weight: 600; + color: var(--md-primary-fg-color); +} + +/* Confidence badges */ +.md-typeset table.rules-table td:nth-child(4), +table.rules-table td:nth-child(4) { + font-weight: 500; +} + +/* Validates / Revokes columns — center */ +.md-typeset table.rules-table td:nth-child(5), +.md-typeset table.rules-table td:nth-child(6), +.md-typeset table.rules-table th:nth-child(5), +.md-typeset table.rules-table th:nth-child(6), +table.rules-table td:nth-child(5), +table.rules-table td:nth-child(6), +table.rules-table th:nth-child(5), +table.rules-table th:nth-child(6) { + text-align: center; + white-space: nowrap; +} + +/* Responsive wrapper */ +@media screen and (max-width: 960px) { + .md-typeset table.rules-table, + table.rules-table { + display: block; + overflow-x: auto; + white-space: nowrap; + } +} + /* Responsive */ @media screen and (max-width: 768px) { .kf-hero__title { diff --git a/docs-site/docs/features/report-viewer.md b/docs-site/docs/features/report-viewer.md index eb3bd72..af96244 100644 --- a/docs-site/docs/features/report-viewer.md +++ b/docs-site/docs/features/report-viewer.md @@ -82,7 +82,7 @@ Drag a Kingfisher, Gitleaks, or TruffleHog JSON report into the page (or use the You can test the hosted page with a bundled sample report: -- [Open sample report JSON](../viewer/sample-report.json) | No | +- [Open sample report JSON](../viewer/sample-report.json) ## Caveats for imported Gitleaks / TruffleHog reports diff --git a/docs-site/docs/rules/builtin-rules.md b/docs-site/docs/rules/builtin-rules.md index 7f68375..fdd168c 100644 --- a/docs-site/docs/rules/builtin-rules.md +++ b/docs-site/docs/rules/builtin-rules.md @@ -63,7 +63,7 @@ Of these, **605** include live validation and **57** support direct revocation. Adobe Adobe Stock API Key kingfisher.adobe.1 -Unknown +Medium Yes @@ -71,7 +71,7 @@ Of these, **605** include live validation and **57** support direct revocation. Adobe Adobe IO Product ID kingfisher.adobe.2 -Unknown +Medium @@ -79,7 +79,7 @@ Of these, **605** include live validation and **57** support direct revocation. Adobe Adobe OAuth Client Secret kingfisher.adobe.3 -Unknown +Medium Yes @@ -87,7 +87,7 @@ Of these, **605** include live validation and **57** support direct revocation. Adobe Adobe OAuth Client ID kingfisher.adobe.4 -Unknown +Medium @@ -1511,7 +1511,7 @@ Of these, **605** include live validation and **57** support direct revocation. Coinbase Coinbase Access Token kingfisher.coinbase.1 -Unknown +Medium Yes @@ -1519,7 +1519,7 @@ Of these, **605** include live validation and **57** support direct revocation. Coinbase Coinbase CDP API Key (ECDSA) kingfisher.coinbase.2 -Unknown +Medium Yes @@ -1527,7 +1527,7 @@ Of these, **605** include live validation and **57** support direct revocation. Coinbase Coinbase CDP API Key (Ed25519) kingfisher.coinbase.3 -Unknown +Medium Yes @@ -1975,7 +1975,7 @@ Of these, **605** include live validation and **57** support direct revocation. Diffbot Diffbot API Key kingfisher.diffbot.1 -Unknown +Medium Yes @@ -2415,7 +2415,7 @@ Of these, **605** include live validation and **57** support direct revocation. Facebook Facebook Secret Key kingfisher.facebook.2 -Unknown +Medium Yes @@ -2463,7 +2463,7 @@ Of these, **605** include live validation and **57** support direct revocation. Figma Figma Personal Access Header Token kingfisher.figma.2 -Unknown +Medium Yes @@ -2879,7 +2879,7 @@ Of these, **605** include live validation and **57** support direct revocation. Github GitHub Personal Access Token - fine-grained permissions kingfisher.github.1 -Unknown +Medium Yes Yes @@ -2887,7 +2887,7 @@ Of these, **605** include live validation and **57** support direct revocation. Github GitHub Personal Access Token kingfisher.github.2 -Unknown +Medium Yes Yes @@ -2903,7 +2903,7 @@ Of these, **605** include live validation and **57** support direct revocation. Github GitHub App User-to-Server Token kingfisher.github.4 -Unknown +Medium Yes Yes @@ -2911,7 +2911,7 @@ Of these, **605** include live validation and **57** support direct revocation. Github GitHub App Server-to-Server Token kingfisher.github.5 -Unknown +Medium Yes Yes @@ -2919,7 +2919,7 @@ Of these, **605** include live validation and **57** support direct revocation. Github GitHub Refresh Token kingfisher.github.6 -Unknown +Medium Yes Yes @@ -2927,7 +2927,7 @@ Of these, **605** include live validation and **57** support direct revocation. Github GitHub Client ID kingfisher.github.7 -Unknown +Medium @@ -2935,7 +2935,7 @@ Of these, **605** include live validation and **57** support direct revocation. Github GitHub Legacy Secret Key kingfisher.github.8 -Unknown +Medium Yes @@ -2999,7 +2999,7 @@ Of these, **605** include live validation and **57** support direct revocation. Gitlab GitLab Runner Registration Token kingfisher.gitlab.2 -Unknown +Medium Yes @@ -3007,7 +3007,7 @@ Of these, **605** include live validation and **57** support direct revocation. Gitlab GitLab Pipeline Trigger Token kingfisher.gitlab.3 -Unknown +Medium Yes @@ -3887,7 +3887,7 @@ Of these, **605** include live validation and **57** support direct revocation. Langchain LangSmith Personal Access Token kingfisher.langchain.1 -Unknown +Medium Yes @@ -3895,7 +3895,7 @@ Of these, **605** include live validation and **57** support direct revocation. Langchain LangSmith Service Key kingfisher.langchain.2 -Unknown +Medium Yes @@ -4463,7 +4463,7 @@ Of these, **605** include live validation and **57** support direct revocation. Mongodb MongoDB API Private Key kingfisher.mongodb.1 -Unknown +Medium Yes Yes @@ -4479,7 +4479,7 @@ Of these, **605** include live validation and **57** support direct revocation. Mongodb MongoDB URI Connection String kingfisher.mongodb.3 -Unknown +Medium Yes @@ -4487,7 +4487,7 @@ Of these, **605** include live validation and **57** support direct revocation. Mongodb MongoDB Atlas Service Account Token kingfisher.mongodb.4 -Unknown +Medium Yes @@ -4535,7 +4535,7 @@ Of these, **605** include live validation and **57** support direct revocation. Nasa NASA API Key kingfisher.nasa.1 -Unknown +Medium Yes @@ -4567,7 +4567,7 @@ Of these, **605** include live validation and **57** support direct revocation. Netlify Netlify API Key kingfisher.netlify.1 -Unknown +Medium Yes Yes @@ -4615,7 +4615,7 @@ Of these, **605** include live validation and **57** support direct revocation. Ngrok Ngrok API Key kingfisher.ngrok.1 -Unknown +Medium Yes @@ -4759,7 +4759,7 @@ Of these, **605** include live validation and **57** support direct revocation. Okta Okta API Token kingfisher.okta.1 -Unknown +Medium Yes @@ -4767,7 +4767,7 @@ Of these, **605** include live validation and **57** support direct revocation. Okta Okta Domain kingfisher.okta.2 -Unknown +Medium @@ -4879,7 +4879,7 @@ Of these, **605** include live validation and **57** support direct revocation. Opsgenie OpsGenie API Key kingfisher.opsgenie.1 -Unknown +Medium Yes @@ -5015,7 +5015,7 @@ Of these, **605** include live validation and **57** support direct revocation. Paypal PayPal OAuth Client ID kingfisher.paypal.1 -Unknown +Medium @@ -5023,7 +5023,7 @@ Of these, **605** include live validation and **57** support direct revocation. Paypal PayPal OAuth Secret kingfisher.paypal.2 -Unknown +Medium Yes @@ -5239,7 +5239,7 @@ Of these, **605** include live validation and **57** support direct revocation. Planetscale PlanetScale API Token kingfisher.planetscale.1 -Unknown +Medium Yes @@ -5247,7 +5247,7 @@ Of these, **605** include live validation and **57** support direct revocation. Planetscale PlanetScale Username kingfisher.planetscale.2 -Unknown +Medium @@ -5439,7 +5439,7 @@ Of these, **605** include live validation and **57** support direct revocation. Pubnub PubNub Publish Key kingfisher.pubnub.1 -Unknown +Medium Yes @@ -5455,7 +5455,7 @@ Of these, **605** include live validation and **57** support direct revocation. Pulumi Pulumi API Key kingfisher.pulumi.1 -Unknown +Medium Yes @@ -6079,7 +6079,7 @@ Of these, **605** include live validation and **57** support direct revocation. Shopify Shopify access token kingfisher.shopify.1 -Unknown +Medium Yes @@ -6087,7 +6087,7 @@ Of these, **605** include live validation and **57** support direct revocation. Shopify Shopify Domain kingfisher.shopify.2 -Unknown +Medium @@ -6135,7 +6135,7 @@ Of these, **605** include live validation and **57** support direct revocation. Slack Slack App Token kingfisher.slack.1 -Unknown +Medium Yes Yes @@ -6143,7 +6143,7 @@ Of these, **605** include live validation and **57** support direct revocation. Slack Slack Token kingfisher.slack.2 -Unknown +Medium Yes Yes @@ -6151,7 +6151,7 @@ Of these, **605** include live validation and **57** support direct revocation. Slack Slack Webhook kingfisher.slack.4 -Unknown +Medium Yes @@ -6199,7 +6199,7 @@ Of these, **605** include live validation and **57** support direct revocation. Snyk Snyk API Key kingfisher.snyk.1 -Unknown +Medium Yes @@ -6215,7 +6215,7 @@ Of these, **605** include live validation and **57** support direct revocation. Sonarcloud SonarCloud API Token kingfisher.sonarcloud.1 -Unknown +Medium Yes @@ -6223,7 +6223,7 @@ Of these, **605** include live validation and **57** support direct revocation. Sonarqube SonarQube API Key kingfisher.sonarqube.1 -Unknown +Medium Yes @@ -6231,7 +6231,7 @@ Of these, **605** include live validation and **57** support direct revocation. Sonarqube SonarQube Host kingfisher.sonarqube.2 -Unknown +Medium @@ -6247,7 +6247,7 @@ Of these, **605** include live validation and **57** support direct revocation. Sourcegraph Sourcegraph Access Token kingfisher.sourcegraph.1 -Unknown +Medium Yes @@ -6567,7 +6567,7 @@ Of these, **605** include live validation and **57** support direct revocation. Teamcity TeamCity API Token kingfisher.teamcity.1 -Unknown +Medium @@ -6847,7 +6847,7 @@ Of these, **605** include live validation and **57** support direct revocation. Twilio Twilio API ID kingfisher.twilio.1 -Unknown +Medium @@ -6855,7 +6855,7 @@ Of these, **605** include live validation and **57** support direct revocation. Twilio Twilio API Key kingfisher.twilio.2 -Unknown +Medium Yes Yes @@ -6879,7 +6879,7 @@ Of these, **605** include live validation and **57** support direct revocation. Twitter Twitter Consumer Key kingfisher.twitter.2 -Unknown +Medium @@ -6887,7 +6887,7 @@ Of these, **605** include live validation and **57** support direct revocation. Twitter X / Twitter Consumer Secret kingfisher.twitter.3 -Unknown +Medium Yes @@ -7119,7 +7119,7 @@ Of these, **605** include live validation and **57** support direct revocation. Vmware Credentials in Connect-VIServer Invocation kingfisher.vmware.1 -Unknown +Medium diff --git a/docs-site/mkdocs.yml b/docs-site/mkdocs.yml index 8be9eb6..12fd251 100644 --- a/docs-site/mkdocs.yml +++ b/docs-site/mkdocs.yml @@ -91,8 +91,8 @@ nav: - Finding Fingerprints: features/fingerprints.md - LLM & Agent Integration: features/agents.md - Rules: - - Writing Custom Rules: rules/overview.md - Built-in Rules List: rules/builtin-rules.md + - Writing Custom Rules: rules/overview.md - Reference: - Architecture: reference/architecture.md - Rust Library Crates: reference/library.md diff --git a/docs-site/scripts/generate-rules-page.py b/docs-site/scripts/generate-rules-page.py index 00debbf..c15a067 100644 --- a/docs-site/scripts/generate-rules-page.py +++ b/docs-site/scripts/generate-rules-page.py @@ -46,7 +46,7 @@ def load_rules(): name = rule.get("name", "Unknown") rule_id = rule.get("id", "") - confidence = rule.get("confidence", "unknown") + confidence = rule.get("confidence") or "medium" has_validation = "validation" in rule has_revocation = "revocation" in rule is_dependent = bool(rule.get("depends_on_rule")) diff --git a/src/baseline.rs b/src/baseline.rs index f20a88d..32cc398 100644 --- a/src/baseline.rs +++ b/src/baseline.rs @@ -43,14 +43,19 @@ pub fn load_baseline(path: &Path) -> Result { /// or the 16-char zero-padded hex form previously written by `--manage-baseline`. /// Detection: /// 1. A `0x`/`0X` prefix is stripped and the rest parsed as hex. -/// 2. Exactly 16 characters of `[0-9a-fA-F]` are parsed as hex (legacy canonical form). +/// 2. Exactly 16 hex chars containing at least one `a-f`/`A-F` letter are parsed as hex +/// (legacy canonical form). An all-digit 16-char string is ambiguous and is treated +/// as decimal so that decimal fingerprints from scan output round-trip correctly. /// 3. Otherwise the string is parsed as decimal u64. fn parse_fingerprint(s: &str) -> Option { let trimmed = s.trim(); if let Some(rest) = trimmed.strip_prefix("0x").or_else(|| trimmed.strip_prefix("0X")) { return u64::from_str_radix(rest, 16).ok(); } - if trimmed.len() == 16 && trimmed.chars().all(|c| c.is_ascii_hexdigit()) { + if trimmed.len() == 16 + && trimmed.chars().all(|c| c.is_ascii_hexdigit()) + && trimmed.chars().any(|c| c.is_ascii_alphabetic()) + { return u64::from_str_radix(trimmed, 16).ok(); } trimmed.parse::().ok() @@ -370,7 +375,10 @@ mod tests { fs::write(&file_hex, "dummy")?; fs::write(&file_dec, "dummy")?; let baseline_path = tmp.path().join("baseline.yaml"); - let fp_hex = 0x1111_2222_3333_4444_u64; + // fp_hex must contain at least one hex letter so its 16-char hex form is + // unambiguously hex (an all-digit 16-char string is treated as decimal to + // satisfy the roundtrip contract for fingerprints copied from scan output). + let fp_hex = 0x1a2b_3c4d_5e6f_7890_u64; let fp_dec = 0xaaaa_bbbb_cccc_dddd_u64; let mixed = BaselineFile { diff --git a/testdata/parsers/scan_findings_baseline.json b/testdata/parsers/scan_findings_baseline.json index efe8e9d..ef4e2c5 100644 --- a/testdata/parsers/scan_findings_baseline.json +++ b/testdata/parsers/scan_findings_baseline.json @@ -141,7 +141,7 @@ }, { "rule_id": "kingfisher.stripe.1", - "snippet": "pk_live_bu9JFVJtII3FINL1rOKcNpveXD4hSMtSDx7opOWDEFGHIJKLMNOPQRSTUVWX" + "snippet": "pk_live_bu9JFVJtII3FINL1rOKcNpveXD4hSMtSDx7opOWDEFGHIJKLMNOPQRSTUVWXYZ" }, { "rule_id": "kingfisher.stripe.2", @@ -149,7 +149,7 @@ }, { "rule_id": "kingfisher.stripe.2", - "snippet": "sk_live_bu9JFVJtII3FINL1rOKcNpveXD4hSMtSDx7opOWDEFGHIJKLMNOPQRST" + "snippet": "sk_live_bu9JFVJtII3FINL1rOKcNpveXD4hSMtSDx7opOWDEFGHIJKLMNOPQRSTUVWXYZ" }, { "rule_id": "kingfisher.twilio.2",