diff --git a/README.md b/README.md index d0f2075..6b09dde 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Kingfisher is a high-performance, open source secret detection tool for source c - **Python Bytecode (.pyc) Scanning**: Extracts and scans string constants from compiled Python (`.pyc`, `.pyo`) files - **Baseline management**: generate and track baselines to suppress known secrets ([docs/BASELINE.md](/docs/BASELINE.md)) - **Checksum-aware detection**: verifies tokens with built-in checksums (e.g., GitHub, Confluent, Zuplo) — no API calls required -- **Built-in Report Viewer**: Visualize and triage findings locally with `kingfisher view ./report-file.json` +- **Built-in Report Viewer**: Visualize and triage findings locally with `kingfisher view ./report-file.json` (supports multiple files and directories) - **Audit reporting**: Generate compliance-oriented HTML reports with scan metadata and validation ordering - **Library crates**: Embed Kingfisher's scanning engine in your own Rust applications ([docs/LIBRARY.md](docs/LIBRARY.md)) @@ -432,6 +432,12 @@ kingfisher scan /path/to/code --access-map --view-report # View access-map reports locally kingfisher view kingfisher.json + +# Combine multiple reports (deduplicated by fingerprint) +kingfisher view report1.json report2.jsonl + +# Load all reports from a directory (non-recursive, skips non-JSON/JSONL files) +kingfisher view ./reports/ ``` > **Use the access map functionality only when you are authorized to inspect the target account, as Kingfisher will issue additional network requests to determine what access the secret grants** diff --git a/crates/kingfisher-rules/data/rules/closecrm.yml b/crates/kingfisher-rules/data/rules/closecrm.yml index a654887..f78f958 100644 --- a/crates/kingfisher-rules/data/rules/closecrm.yml +++ b/crates/kingfisher-rules/data/rules/closecrm.yml @@ -4,6 +4,11 @@ rules: pattern: | (?xi) \b + close + (?:.|[\n\r]){0,32}? + (?:API[_-]?KEY|SECRET|TOKEN|KEY) + (?:.|[\n\r]){0,16}? + \b ( api_[A-Za-z0-9]{18,26}\.[A-Za-z0-9]{18,26} ) diff --git a/crates/kingfisher-rules/data/rules/inngest.yml b/crates/kingfisher-rules/data/rules/inngest.yml index 23fa12b..9b134f5 100644 --- a/crates/kingfisher-rules/data/rules/inngest.yml +++ b/crates/kingfisher-rules/data/rules/inngest.yml @@ -12,7 +12,7 @@ rules: confidence: medium examples: - 'INNGEST_SIGNING_KEY=signkey-prod-b2ed992186a5cb19f6668aade821f502c1d00970dfd0e35128d51bac4649916c' - - 'INNGEST_SIGNING_KEY="signkey-staging-12345678abcdef0123456789abcdef0123456789abcdef0123456789abcdef"' + - 'INNGEST_SIGNING_KEY="signkey-staging-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"' references: - https://www.inngest.com/docs/platform/signing-keys validation: @@ -55,7 +55,7 @@ rules: min_entropy: 3.0 confidence: medium examples: - - 'INNGEST_EVENT_KEY=AbCdEfGhIjKlMnOpQrStUvWxYz0123456789_-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789_-AbCdEfGhIj' + - 'INNGEST EVENT_KEY=AbCdEfGhIjKlMnOpQrStUvWxYz0123456789_-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789_-AbCdEfGhIj' - 'inngest event key: "ZyXwVuTsRqPoNmLkJiHgFeDcBa9876543210_-ZyXwVuTsRqPoNmLkJiHgFeDcBa9876543210_-ZyXwVuTsRq"' references: - https://www.inngest.com/docs/events/creating-an-event-key diff --git a/crates/kingfisher-rules/data/rules/livekit.yml b/crates/kingfisher-rules/data/rules/livekit.yml index a5623af..46bc759 100644 --- a/crates/kingfisher-rules/data/rules/livekit.yml +++ b/crates/kingfisher-rules/data/rules/livekit.yml @@ -70,23 +70,21 @@ rules: method: POST url: > {%- assign base_url = LIVEKIT_URL | replace: "wss://", "https://" | replace: "ws://", "http://" -%} - {{ base_url }}/twirp/livekit.RoomService/ListRooms + {{ base_url }}/twirp/livekit.RoomService/ListParticipants headers: Content-Type: application/json Accept: application/json - Authorization: '{%- assign header = "HS256" | jwt_header -%}{%- assign now = "" | unix_timestamp -%}{%- assign exp = now | plus: 300 -%}{%- assign nbf = now | minus: 5 -%}{%- assign payload_json = ''{"iss":"'' | append: API_KEY | append: ''","sub":"kingfisher-validation","exp":'' | append: exp | append: '',"nbf":'' | append: nbf | append: '',"video":{"roomList":true}}'' -%}{%- assign payload = payload_json | b64url_enc -%}{%- assign signing_input = header | append: "." | append: payload -%}{%- assign sig_b64 = signing_input | hmac_sha256: TOKEN -%}{%- assign sig = sig_b64 | replace: "+", "-" | replace: "/", "_" | replace: "=", "" -%}Bearer {{ header }}.{{ payload }}.{{ sig }}' + Authorization: '{%- assign header = "HS256" | jwt_header -%}{%- assign now = "" | unix_timestamp -%}{%- assign exp = now | plus: 300 -%}{%- assign nbf = now | minus: 5 -%}{%- assign payload_json = ''{"iss":"'' | append: API_KEY | append: ''","sub":"kingfisher-validation","exp":'' | append: exp | append: '',"nbf":'' | append: nbf | append: '',"video":{"roomAdmin":true,"room":"__kingfisher_validation__"}}'' -%}{%- assign payload = payload_json | b64url_enc -%}{%- assign signing_input = header | append: "." | append: payload -%}{%- assign sig_b64 = signing_input | hmac_sha256: TOKEN -%}{%- assign sig = sig_b64 | replace: "+", "-" | replace: "/", "_" | replace: "=", "" -%}Bearer {{ header }}.{{ payload }}.{{ sig }}' body: | - {} + {"room":"__kingfisher_validation__"} response_matcher: - report_response: true - type: StatusMatch - status: [200] + status: [200, 404] - type: StatusMatch status: [401, 403] negative: true - type: JsonValid - - type: WordMatch - words: ['"rooms"'] # LiveKit validation needs the URL and API key as well, so standalone API secrets must remain # detectable even when contextual verification is unavailable. diff --git a/crates/kingfisher-rules/data/rules/triggerdev.yml b/crates/kingfisher-rules/data/rules/triggerdev.yml index 0a918f7..8d33b88 100644 --- a/crates/kingfisher-rules/data/rules/triggerdev.yml +++ b/crates/kingfisher-rules/data/rules/triggerdev.yml @@ -5,16 +5,18 @@ rules: (?x) \b ( - tr_(?Pdev|prod|stg)_[A-Za-z0-9]{20,40} + tr_(?:dev|prod|stg)_[A-Za-z0-9]{20} ) \b pattern_requirements: min_digits: 1 + min_uppercase: 1 + min_lowercase: 1 min_entropy: 3.0 confidence: medium examples: - - 'TRIGGER_SECRET_KEY=tr_dev_1a2b3c4d5e6f7g8h9i0j' - - 'TRIGGER_SECRET_KEY=tr_prod_xK8m2LpQr5nW0vYz3cJ7' + - 'TRIGGER_SECRET_KEY=tr_dev_AN0MnvS4n4GdfhELPUMU' + - 'TRIGGER_SECRET_KEY=tr_prod_KCqL36ucD5LTPa9kdnMj' references: - https://trigger.dev/docs/management/authentication - https://trigger.dev/docs/management/envvars/list @@ -26,7 +28,7 @@ rules: content: request: method: GET - url: 'https://api.trigger.dev/api/v1/projects/{{ TRIGGER_PROJECT_REF }}/envvars/{{ env | replace: "stg", "staging" }}' + url: 'https://api.trigger.dev/api/v1/projects/{{ TRIGGER_PROJECT_REF }}/envvars/{{ TOKEN | split: "_" | slice: 1, 1 | first | replace: "stg", "staging" }}' headers: Authorization: "Bearer {{ TOKEN }}" Accept: application/json @@ -42,15 +44,17 @@ rules: (?x) \b ( - tr_pat_[A-Za-z0-9]{20,40} + tr_pat_[A-Za-z0-9]{20} ) \b pattern_requirements: min_digits: 1 + min_uppercase: 1 + min_lowercase: 1 min_entropy: 3.0 confidence: medium examples: - - 'TRIGGER_ACCESS_TOKEN=tr_pat_xK8m2LpQr5nW0vYz3cJ7aB4d' + - 'TRIGGER_ACCESS_TOKEN=tr_pat_G8DwRcZEc0ONFMtkVHt8' references: - https://trigger.dev/docs/management/authentication - https://trigger.dev/docs/management/envvars/list diff --git a/crates/kingfisher-rules/data/rules/uri.yml b/crates/kingfisher-rules/data/rules/uri.yml index a214645..a5db95e 100644 --- a/crates/kingfisher-rules/data/rules/uri.yml +++ b/crates/kingfisher-rules/data/rules/uri.yml @@ -34,6 +34,7 @@ rules: content: request: method: GET + response_is_html: true url: '{{ TOKEN }}' response_matcher: - report_response: false diff --git a/docs-site/docs/usage/advanced.md b/docs-site/docs/usage/advanced.md index 0743796..84c6d21 100644 --- a/docs-site/docs/usage/advanced.md +++ b/docs-site/docs/usage/advanced.md @@ -121,7 +121,7 @@ kingfisher scan /path/to/repo \ ``` **What you'll see** -Findings tied to a skip-listed account report `Validation: Not Attempted` and note in the `Response:` that the entry came from the skip list: +Findings tied to a skip-listed account report `Validation: Canary Token (Skipped)` and note in the `Response:` that the entry came from the skip list: ```bash AWS SECRET ACCESS KEY => [KINGFISHER.AWS.2] @@ -129,7 +129,7 @@ AWS SECRET ACCESS KEY => [KINGFISHER.AWS.2] |Fingerprint...: 2141074333616819500 |Confidence....: medium |Entropy.......: 5.00 - |Validation....: Not Attempted + |Validation....: Canary Token (Skipped) |__Response....: (skip list entry) AWS validation not attempted for account 171436882533. |Language......: Unknown |Line Num......: 21 @@ -137,7 +137,7 @@ AWS SECRET ACCESS KEY => [KINGFISHER.AWS.2] ``` **Why this matters** -Skipping prevents noisy tripwires in prod telemetry while keeping the status explicit—"Not Attempted" isn't a pass. If needed, verify these credentials out-of-band or with a safe, non-triggering method. +Skipping prevents noisy tripwires in prod telemetry while keeping the status explicit—"Canary Token (Skipped)" signals that the credential likely belongs to an active honeypot but was intentionally not validated. If needed, verify these credentials out-of-band or with a safe, non-triggering method. #### Common CLI flows diff --git a/docs-site/docs/usage/basic-scanning.md b/docs-site/docs/usage/basic-scanning.md index 54bf874..7763da3 100644 --- a/docs-site/docs/usage/basic-scanning.md +++ b/docs-site/docs/usage/basic-scanning.md @@ -121,6 +121,18 @@ kingfisher view kingfisher.json The `view` subcommand starts a server (default port `7890`, bind address `127.0.0.1`) that bundles the HTML, CSS, and JavaScript for the access-map viewer directly into the Kingfisher binary. Provide a JSON or JSONL report to load it automatically and Kingfisher will open your browser, or open the page and upload a report in the browser. If port 7890 is already in use, re-run with `--port `. To allow access from Docker or other hosts, use `--address 0.0.0.0`. +You can pass multiple files or a directory to combine reports. Findings are deduplicated by fingerprint. Non-matching files in a directory are silently skipped (no recursion). + +```bash +# Combine multiple report files +kingfisher view report1.json report2.jsonl + +# Load all JSON/JSONL reports from a directory +kingfisher view ./reports/ +``` + +The browser-based viewer also supports loading multiple files via drag-and-drop or the file picker, with the same fingerprint-based deduplication. + ### Pipe any text directly into Kingfisher by passing `-` ```bash diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index 26a2692..001c8a4 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -118,7 +118,7 @@ kingfisher scan /path/to/repo \ ``` **What you'll see** -Findings tied to a skip-listed account report `Validation: Not Attempted` and note in the `Response:` that the entry came from the skip list: +Findings tied to a skip-listed account report `Validation: Canary Token (Skipped)` and note in the `Response:` that the entry came from the skip list: ```bash AWS SECRET ACCESS KEY => [KINGFISHER.AWS.2] @@ -126,7 +126,7 @@ AWS SECRET ACCESS KEY => [KINGFISHER.AWS.2] |Fingerprint...: 2141074333616819500 |Confidence....: medium |Entropy.......: 5.00 - |Validation....: Not Attempted + |Validation....: Canary Token (Skipped) |__Response....: (skip list entry) AWS validation not attempted for account 171436882533. |Language......: Unknown |Line Num......: 21 @@ -134,7 +134,7 @@ AWS SECRET ACCESS KEY => [KINGFISHER.AWS.2] ``` **Why this matters** -Skipping prevents noisy tripwires in prod telemetry while keeping the status explicit—"Not Attempted" isn't a pass. If needed, verify these credentials out-of-band or with a safe, non-triggering method. +Skipping prevents noisy tripwires in prod telemetry while keeping the status explicit—"Canary Token (Skipped)" signals that the credential likely belongs to an active honeypot but was intentionally not validated. If needed, verify these credentials out-of-band or with a safe, non-triggering method. #### Common CLI flows diff --git a/docs/USAGE.md b/docs/USAGE.md index 69c07fe..bc29887 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -116,6 +116,18 @@ kingfisher view kingfisher.json The `view` subcommand starts a server (default port `7890`, bind address `127.0.0.1`) that bundles the HTML, CSS, and JavaScript for the access-map viewer directly into the Kingfisher binary. Provide a JSON or JSONL report to load it automatically and Kingfisher will open your browser, or open the page and upload a report in the browser. If port 7890 is already in use, re-run with `--port `. To allow access from Docker or other hosts, use `--address 0.0.0.0`. +You can pass multiple files or a directory to combine reports. Findings are deduplicated by fingerprint. Non-matching files in a directory are silently skipped (no recursion). + +```bash +# Combine multiple report files +kingfisher view report1.json report2.jsonl + +# Load all JSON/JSONL reports from a directory +kingfisher view ./reports/ +``` + +The browser-based viewer also supports loading multiple files via drag-and-drop or the file picker, with the same fingerprint-based deduplication. + ### Pipe any text directly into Kingfisher by passing `-` ```bash diff --git a/docs/access-map-viewer/index.html b/docs/access-map-viewer/index.html index 76fd3ed..1808cc1 100644 --- a/docs/access-map-viewer/index.html +++ b/docs/access-map-viewer/index.html @@ -1257,10 +1257,10 @@
📄
-
Drag & drop a report here
-
…or click to choose a file
-
Your file stays in the browser—load JSON or JSONL reports locally.
- +
Drag & drop reports here
+
…or click to choose files
+
Your files stay in the browser—load JSON or JSONL reports locally. Multiple files are merged and deduplicated.
+
@@ -1397,6 +1397,7 @@ + Rows