From 1337588c7bc4be82e81289030acd78a3cb23e583 Mon Sep 17 00:00:00 2001 From: Mick Grove Date: Wed, 29 Apr 2026 11:46:17 -0700 Subject: [PATCH] =?UTF-8?q?Added=20first-class=20**Postman**=20scanning=20?= =?UTF-8?q?target:=20new=20kingfisher=20scan=20postman=20subcommand=20(and?= =?UTF-8?q?=20equivalent=20--postman-*=20flags)=20fetches=20workspaces,=20?= =?UTF-8?q?collections,=20and=20environments=20via=20the=20Postman=20API?= =?UTF-8?q?=20and=20scans=20them=20for=20hard-coded=20credentials=20in=20r?= =?UTF-8?q?equest=20auth=20blocks,=20pre-request/test=20scripts,=20saved?= =?UTF-8?q?=20example=20responses,=20and=20=E2=80=94=20notably=20=E2=80=94?= =?UTF-8?q?=20secret-typed=20environment=20variables,=20which=20the=20API?= =?UTF-8?q?=20returns=20in=20plaintext=20despite=20the=20UI=20mask.=20Sele?= =?UTF-8?q?ctors:=20--workspace,=20--collection,=20--environment,=20--all,?= =?UTF-8?q?=20with=20optional=20--include-mocks-monitors=20and=20--api-url?= =?UTF-8?q?=20for=20self-hosted=20endpoints.=20Authenticates=20via=20KF=5F?= =?UTF-8?q?POSTMAN=5FTOKEN=20(or=20POSTMAN=5FAPI=5FKEY)=20sent=20as=20X-Ap?= =?UTF-8?q?i-Key;=20honors=20X-RateLimit-RetryAfter=20on=20429s.=20Finding?= =?UTF-8?q?s=20link=20back=20to=20https://go.postman.co/...=20URLs=20in=20?= =?UTF-8?q?reports.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs-site/docs/usage/integrations.md | 30 ++++++------ docs/INTEGRATIONS.md | 28 ++++++------ src/postman.rs | 68 ++++++++++++++++++++++++---- 3 files changed, 91 insertions(+), 35 deletions(-) diff --git a/docs-site/docs/usage/integrations.md b/docs-site/docs/usage/integrations.md index 4a5040d..0d77442 100644 --- a/docs-site/docs/usage/integrations.md +++ b/docs-site/docs/usage/integrations.md @@ -613,27 +613,27 @@ Kingfisher fetches Postman workspaces, collections, and environments via the pub ### Scan every workspace, collection, and environment visible to the API key ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan --postman-all +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all ``` ### Scan a specific workspace (by ID or web URL) ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ - --postman-workspace 11111111-2222-3333-4444-555555555555 +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman \ + --workspace 11111111-2222-3333-4444-555555555555 -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ - --postman-workspace https://www.postman.com/team-handle/workspace/abc-uid-123 +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman \ + --workspace https://www.postman.com/team-handle/workspace/abc-uid-123 ``` ### Scan a single collection or environment ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ - --postman-collection 12345678-abcd-efgh-ijkl-mnopqrstuvwx +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman \ + --collection 12345678-abcd-efgh-ijkl-mnopqrstuvwx -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ - --postman-environment 12345678-abcd-efgh-ijkl-mnopqrstuvwx +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman \ + --environment 12345678-abcd-efgh-ijkl-mnopqrstuvwx ``` ### Include mocks and monitors @@ -641,20 +641,22 @@ KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ Mocks and monitors are scanned only when explicitly requested (they are lower-yield surfaces): ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan --postman-all \ - --postman-include-mocks-monitors +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all \ + --include-mocks-monitors ``` ### Self-hosted / enterprise endpoint ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan --postman-all \ - --postman-api-url https://postman.internal.example.com/ +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all \ + --api-url https://postman.internal.example.com/ ``` The token is sent as the `X-Api-Key` header. Either `KF_POSTMAN_TOKEN` or `POSTMAN_API_KEY` is accepted (the latter matches the env var Postman's own docs reference). Mint a key from postman.com → Settings → API keys. -**Out of scope:** Postman Vault secrets are client-side and not reachable via the API. The Postman API Network does not expose a search endpoint; supply specific public-workspace IDs via `--postman-workspace` to scan public surfaces. +> Top-level `kingfisher scan --postman-*` flags remain accepted as hidden aliases for backward compatibility, but new usage should prefer the `kingfisher scan postman` subcommand shown above. + +**Out of scope:** Postman Vault secrets are client-side and not reachable via the API. The Postman API Network does not expose a search endpoint; supply specific public-workspace IDs via `kingfisher scan postman --workspace` to scan public surfaces. ## Environment Variables diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md index a64e11b..593f810 100644 --- a/docs/INTEGRATIONS.md +++ b/docs/INTEGRATIONS.md @@ -610,27 +610,27 @@ Kingfisher fetches Postman workspaces, collections, and environments via the pub ### Scan every workspace, collection, and environment visible to the API key ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan --postman-all +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all ``` ### Scan a specific workspace (by ID or web URL) ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ - --postman-workspace 11111111-2222-3333-4444-555555555555 +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman \ + --workspace 11111111-2222-3333-4444-555555555555 -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ - --postman-workspace https://www.postman.com/team-handle/workspace/abc-uid-123 +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman \ + --workspace https://www.postman.com/team-handle/workspace/abc-uid-123 ``` ### Scan a single collection or environment ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ - --postman-collection 12345678-abcd-efgh-ijkl-mnopqrstuvwx +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman \ + --collection 12345678-abcd-efgh-ijkl-mnopqrstuvwx -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ - --postman-environment 12345678-abcd-efgh-ijkl-mnopqrstuvwx +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman \ + --environment 12345678-abcd-efgh-ijkl-mnopqrstuvwx ``` ### Include mocks and monitors @@ -638,19 +638,21 @@ KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan \ Mocks and monitors are scanned only when explicitly requested (they are lower-yield surfaces): ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan --postman-all \ - --postman-include-mocks-monitors +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all \ + --include-mocks-monitors ``` ### Self-hosted / enterprise endpoint ```bash -KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan --postman-all \ - --postman-api-url https://postman.internal.example.com/ +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all \ + --api-url https://postman.internal.example.com/ ``` The token is sent as the `X-Api-Key` header. Either `KF_POSTMAN_TOKEN` or `POSTMAN_API_KEY` is accepted (the latter matches the env var Postman's own docs reference). Mint a key from postman.com → Settings → API keys. +> Top-level `kingfisher scan --postman-*` flags remain accepted as hidden aliases for backward compatibility, but new usage should prefer the `kingfisher scan postman` subcommand shown above. + **Out of scope:** Postman Vault secrets are client-side and not reachable via the API. The Postman API Network does not expose a search endpoint; supply specific public-workspace IDs via `--postman-workspace` to scan public surfaces. ## Environment Variables diff --git a/src/postman.rs b/src/postman.rs index d1bec8c..c9b7bdc 100644 --- a/src/postman.rs +++ b/src/postman.rs @@ -86,15 +86,57 @@ fn token_from_env() -> Result { /// Best-effort UID extraction. Accepts: /// - bare UID strings (returned unchanged) -/// - Postman web URLs: take the last URL path segment +/// - Postman web URLs of the form `.../{workspace,collection,environment,mock,monitor}[s]/[/]`: +/// the UID following the type marker is preferred. Falls back to the last +/// non-suffix path segment if no type marker is present. fn resolve_uid(input: &str) -> String { if !input.starts_with("http://") && !input.starts_with("https://") { return input.to_string(); } - if let Ok(parsed) = Url::parse(input) - && let Some(seg) = parsed.path_segments().and_then(|mut segs| segs.rfind(|s| !s.is_empty())) - { - return seg.to_string(); + let Ok(parsed) = Url::parse(input) else { + return input.to_string(); + }; + let Some(segs) = parsed.path_segments() else { + return input.to_string(); + }; + let segs: Vec<&str> = segs.filter(|s| !s.is_empty()).collect(); + + // Prefer the segment immediately after the *last* known type marker. + // Postman web URLs commonly nest workspace + collection + suffix; the deepest + // type marker is the one the user pasted the URL to scan. + const TYPE_MARKERS: &[&str] = &[ + "workspace", + "workspaces", + "collection", + "collections", + "environment", + "environments", + "mock", + "mocks", + "monitor", + "monitors", + ]; + if let Some(window) = segs.windows(2).rev().find(|w| TYPE_MARKERS.contains(&w[0])) { + return window[1].to_string(); + } + + // Fall back to the last segment that is not a known terminal suffix + // (e.g. /overview, /edit, /run on Postman web URLs). + const TERMINAL_SUFFIXES: &[&str] = &[ + "overview", + "edit", + "run", + "documentation", + "info", + "history", + "tests", + "request", + "fork", + "watch", + "comments", + ]; + if let Some(last) = segs.iter().rev().find(|s| !TERMINAL_SUFFIXES.contains(s)) { + return last.to_string(); } input.to_string() } @@ -222,7 +264,7 @@ pub async fn download_postman_to_dir( .build() .context("Failed to build HTTP client")?; - std::fs::create_dir_all(output_dir)?; + tokio::fs::create_dir_all(output_dir).await?; let mut paths: Vec<(PathBuf, String)> = Vec::new(); @@ -378,12 +420,22 @@ mod tests { } #[test] - fn resolve_uid_extracts_last_segment_from_url() { + fn resolve_uid_extracts_uid_after_type_marker() { assert_eq!( resolve_uid("https://www.postman.com/team/workspace/abc-uid-123"), "abc-uid-123" ); - assert_eq!(resolve_uid("https://www.postman.com/team/workspace/abc/overview"), "overview"); + // Terminal `/overview` must not be mistaken for the UID. + assert_eq!( + resolve_uid("https://www.postman.com/team/workspace/abc-uid-123/overview"), + "abc-uid-123" + ); + // Type marker preference: the segment after `collection/` is the UID, not the trailing segment. + assert_eq!( + resolve_uid("https://go.postman.co/workspace/wks-1/collection/col-9/run"), + "col-9" + ); + assert_eq!(resolve_uid("https://go.postman.co/workspace/wks-1/environment/env-9"), "env-9"); } #[test]