Added first-class **Postman** scanning target: new kingfisher scan postman subcommand (and equivalent --postman-* flags) fetches workspaces, collections, and environments via the Postman API and scans them for hard-coded credentials in request auth blocks, pre-request/test scripts, saved example responses, and — notably — secret-typed environment variables, which the API returns in plaintext despite the UI mask. Selectors: --workspace, --collection, --environment, --all, with optional --include-mocks-monitors and --api-url for self-hosted endpoints. Authenticates via KF_POSTMAN_TOKEN (or POSTMAN_API_KEY) sent as X-Api-Key; honors X-RateLimit-RetryAfter on 429s. Findings link back to https://go.postman.co/... URLs in reports.

This commit is contained in:
Mick Grove 2026-04-29 11:46:17 -07:00
commit 1337588c7b
3 changed files with 91 additions and 35 deletions

View file

@ -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

View file

@ -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

View file

@ -86,15 +86,57 @@ fn token_from_env() -> Result<String> {
/// 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]/<uid>[/<suffix>]`:
/// 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]