diff --git a/AGENTS.md b/AGENTS.md index 7ed5815..03db3f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Key capabilities: - Direct secret revocation from CLI - Blast radius mapping (AWS, GCP, Azure, GitHub, GitLab, Slack) - Output formats: TOON, JSON, SARIF, interactive HTML -- Platform integrations: GitHub, GitLab, Azure Repos, Bitbucket, Gitea, Hugging Face, S3, GCS, Docker, Jira, Confluence, Slack +- Platform integrations: GitHub, GitLab, Azure Repos, Bitbucket, Gitea, Hugging Face, S3, GCS, Docker, Jira, Confluence, Slack, Microsoft Teams, Postman ## Scope - Applies to the entire repository rooted at this file. diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b293d..324997c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. ## [unreleased v1.98.0] +- 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. - Fixed [#359](https://github.com/mongodb/kingfisher/issues/359): added `kingfisher.github.9` to detect the new ~520-character stateless GitHub App installation token format (`ghs__`). The legacy 36-character `ghs_` rule (`kingfisher.github.5`) is retained for older / GHES-issued tokens that are still in circulation. - Added provider endpoint overrides for validation and revocation via global `--endpoint PROVIDER=URL` and `--endpoint-config FILE`, with built-in support for self-hosted GitHub, GitLab, Gitea, Jira, Confluence, and Artifactory instances. diff --git a/README.md b/README.md index 6760690..006b556 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Designed for offensive security engineers and blue-team defenders alike, Kingfis Kingfisher is a high-performance, open source secret detection tool for source code and developer platforms. If you are searching for a "GitHub secret scanner," "API key scanner," "token leak detection," or "Git secrets scanner," this project is built for that workflow. -- Scan code, Git history, and integrated platforms (GitHub, GitLab, Azure Repos, Bitbucket, Gitea, Hugging Face, Jira, Confluence, Slack, Microsoft Teams, Docker, AWS S3, and Google Cloud Storage) +- Scan code, Git history, and integrated platforms (GitHub, GitLab, Azure Repos, Bitbucket, Gitea, Hugging Face, Jira, Confluence, Slack, Microsoft Teams, Postman, Docker, AWS S3, and Google Cloud Storage) - Validate discovered credentials against provider APIs to reduce false positives - Revoke supported secrets directly from the CLI - Generate JSON, SARIF, TOON, and HTML outputs for security teams, compliance, and CI @@ -194,7 +194,7 @@ KF_GITLAB_TOKEN="glpat-..." kingfisher scan gitlab --group my-group ### 8: Scan Azure Repos ```bash -KF_AZURE_PAT="pat" kingfisher scan azure --organization my-org +KF_AZURE_PAT="pat" kingfisher scan azure --azure-organization my-org ``` ### 9: Scan Bitbucket workspace @@ -212,7 +212,7 @@ KF_GITEA_TOKEN="token" kingfisher scan gitea --organization my-org ### 11: Scan Hugging Face ```bash -KF_HUGGINGFACE_TOKEN="hf_..." kingfisher scan huggingface --organization my-org +KF_HUGGINGFACE_TOKEN="hf_..." kingfisher scan huggingface --huggingface-organization my-org ``` ### 12: Scan an S3 bucket @@ -623,6 +623,7 @@ Kingfisher can scan multiple platforms and services directly: - Confluence (pages via CQL queries) - Slack (messages via search queries) - Microsoft Teams (messages via Microsoft Graph search) +- Postman (workspaces, collections, and environments — including plaintext "secret"-typed environment variables) See **[docs/INTEGRATIONS.md](docs/INTEGRATIONS.md)** for complete integration documentation and authentication setup. @@ -645,7 +646,7 @@ kingfisher scan github --organization my-org kingfisher scan gitlab --group my-group # Scan Azure Repos -kingfisher scan azure --organization my-org +kingfisher scan azure --azure-organization my-org # Scan Jira issues KF_JIRA_TOKEN="token" kingfisher scan jira --url https://jira.company.com \ @@ -666,6 +667,9 @@ KF_SLACK_TOKEN="xoxp-..." kingfisher scan slack "from:username has:link" # Scan Microsoft Teams messages KF_TEAMS_TOKEN="eyJ0..." kingfisher scan teams "password OR api_key" + +# Scan every Postman workspace, collection, and environment visible to the API key +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all ``` **For detailed integration instructions and authentication setup, see [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md).** @@ -690,6 +694,7 @@ KF_TEAMS_TOKEN="eyJ0..." kingfisher scan teams "password OR api_key" | `KF_CONFLUENCE_TOKEN` | Confluence API token | | `KF_SLACK_TOKEN` | Slack API token | | `KF_TEAMS_TOKEN` | Microsoft Graph API token for Teams message search | +| `KF_POSTMAN_TOKEN` / `POSTMAN_API_KEY` | Postman API key for workspace, collection, and environment scanning | | `KF_DOCKER_TOKEN` | Docker registry token (`user:pass` or bearer token). If unset, credentials from the Docker keychain are used | | `KF_AWS_KEY`, `KF_AWS_SECRET`, and `KF_AWS_SESSION_TOKEN` | AWS credentials for S3 bucket scanning. Session token is optional, for temporary credentials | @@ -799,7 +804,7 @@ Since then it has evolved far beyond that starting point, introducing live valid - **Hundreds of new built-in rules** and an expanded YAML rule schema - **Baseline management** to suppress known findings over time - **Parser-based context verification** layered on Hyperscan for language-aware detection -- **More scan targets** (GitLab, Bitbucket, Gitea, Jira, Confluence, Slack, Microsoft Teams, S3, GCS, Docker, Hugging Face, etc.) +- **More scan targets** (GitLab, Bitbucket, Gitea, Jira, Confluence, Slack, Microsoft Teams, Postman, S3, GCS, Docker, Hugging Face, etc.) - **Compressed Files**, **SQLite database**, and **Python bytecode (.pyc)** scanning support - **New storage model** (in-memory + Bloom filter, replacing SQLite) - **Unified workflow** with JSON/BSON/SARIF outputs diff --git a/docs-site/docs/blog/posts/2026-04-28-beyond-detection-validate-map-revoke.md b/docs-site/docs/blog/posts/2026-04-28-beyond-detection-validate-map-revoke.md index 3716609..55e23d1 100644 --- a/docs-site/docs/blog/posts/2026-04-28-beyond-detection-validate-map-revoke.md +++ b/docs-site/docs/blog/posts/2026-04-28-beyond-detection-validate-map-revoke.md @@ -100,16 +100,16 @@ you've fished out of a paste or a customer report: ```bash # What does this AWS keypair actually own? -kingfisher access-map aws ./aws.json --json-out aws.access-map.json +kingfisher access-map aws ./aws.json --format json > aws.access-map.json # Same for a GitHub token -kingfisher access-map github ./github.token --json-out github.access-map.json +kingfisher access-map github ./github.token --format json > github.access-map.json # Or a GCP service account -kingfisher access-map gcp ./service-account.json --json-out gcp.access-map.json +kingfisher access-map gcp ./service-account.json --format json > gcp.access-map.json ``` -The HTML report viewer (`--format html`) renders the access map as a +The access-map HTML report renders the access map as a clickable tree: identity at the root, then services, then individual resources and permissions. It is a much faster way to explain severity to an incident commander or manager than pasting IAM JSON into chat. diff --git a/docs-site/docs/blog/posts/2026-04-28-scan-github-org-for-secrets.md b/docs-site/docs/blog/posts/2026-04-28-scan-github-org-for-secrets.md index 9726b7f..7cf9123 100644 --- a/docs-site/docs/blog/posts/2026-04-28-scan-github-org-for-secrets.md +++ b/docs-site/docs/blog/posts/2026-04-28-scan-github-org-for-secrets.md @@ -29,7 +29,7 @@ you can focus on what needs rotation first. ## What you need -- Kingfisher installed (`brew install mongodb/brew/kingfisher`, or grab a +- Kingfisher installed (`brew install kingfisher`, or grab a release from [GitHub](https://github.com/mongodb/kingfisher/releases)). - A GitHub personal access token exported as `KF_GITHUB_TOKEN`. A classic token with `repo` and `read:org` scopes is enough for private repos; for diff --git a/docs-site/docs/blog/posts/2026-04-29-scanning-postman-for-leaked-secrets.md b/docs-site/docs/blog/posts/2026-04-29-scanning-postman-for-leaked-secrets.md new file mode 100644 index 0000000..7fc823d --- /dev/null +++ b/docs-site/docs/blog/posts/2026-04-29-scanning-postman-for-leaked-secrets.md @@ -0,0 +1,250 @@ +--- +date: 2026-04-29 +title: "Scanning Postman for Leaked Secrets — Including the Ones the UI Hides" +description: > + Postman workspaces are a quietly underrated leak surface. Kingfisher now + scans collections, environments, mocks, and monitors directly via the + Postman API — and reads the plaintext of "secret"-typed environment + variables that the Postman UI masks but the API does not. +categories: + - Features +tags: + - postman + - secret-scanning + - validation + - integrations +--- + +# Scanning Postman for Leaked Secrets — Including the Ones the UI Hides + +Postman is everywhere — across backend teams, mobile teams, partner +integrations, and the public Postman API Network. It is also a quietly +prolific leak surface. CloudSEK's December 2024 audit found over **30,000 +public Postman workspaces leaking access tokens** across GitHub, Slack, +Salesforce, Stripe, and Razorpay, among others. Postman themselves now run +server-side secret scans on public content, which tells you everything you +need to know about how often this happens. + +Kingfisher now scans Postman workspaces directly — and finds credentials +that other scanners miss, because most other tools only scan collection +exports a developer has dropped into a repo. They never see the live +workspace. + + + +## The leak surface inside a Postman workspace + +A Postman workspace is more than a list of saved requests. Each of the +following can — and routinely does — contain hard-coded credentials: + +- **Request `auth` blocks**: Bearer tokens, API keys, basic-auth passwords + pinned directly to a request. +- **Request headers and URLs**: `Authorization`, `X-Api-Key`, signed query + strings, pre-signed S3 URLs. +- **Request bodies**: form-encoded `client_secret`, JSON payloads with + service account JSON pasted in. +- **Pre-request and test scripts**: JavaScript snippets that hard-code an + AWS keypair "just for testing." +- **Saved example responses**: an example response with a real token that + was captured when the engineer was debugging. +- **Environment variables, including the "secret" type** — see below. +- **Globals**, **mocks**, and **monitors** — same shape as environments. + +## The headline: Postman's "secret" type does not redact over the API + +Postman environments support a `secret` variable type. In the UI, the value +is masked — you see `••••••••`. It feels like a vault. + +It is not. The `secret` flag is a **UI-masking hint only**. When you call +`GET /environments/{uid}` with an API key that has read access, Postman +returns the value in plaintext: + +```json +{ + "environment": { + "name": "prod", + "values": [ + { + "key": "STRIPE_SECRET", + "value": "sk_live_51H...", + "type": "secret", + "enabled": true + } + ] + } +} +``` + +That means a Postman API key with workspace read access is, in practice, +a key to every "secret" variable across every environment that workspace +can see. Postman documents this — only **Postman Vault** secrets are +genuinely client-side and unreachable via the API. Anything stored as a +"secret" environment variable is fully exposed. + +This is the surface Kingfisher now scans. + +## Get an API key + +1. Go to **postman.com → Settings → API keys → Generate API key**. +2. Copy the value (it starts with `PMAK-`). +3. Export it: + +```bash +export KF_POSTMAN_TOKEN="PMAK-..." +``` + +`POSTMAN_API_KEY` also works as an alias if that's already in your shell — +Kingfisher checks both. + +The key acts with the minting user's permissions — there are no per-scope +toggles. Rate limit is 300 req/min/user across all plans. Kingfisher honors +`X-RateLimit-RetryAfter` and backs off automatically on 429. + +## Scan everything visible to the key + +The fastest way to get a baseline of your team's exposure: + +```bash +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all +``` + +That walks every workspace the key can see, fans out to each collection +and environment, writes the JSON to disk, and runs the full Kingfisher +ruleset against it. Live validation runs by default, so you get back a +list of credentials that **actually authenticate today**, not just regex +matches. + +Mocks and monitors are off by default (lower-yield, more API calls). Add +them explicitly when you want a complete sweep: + +```bash +kingfisher scan postman --all --include-mocks-monitors +``` + +## Scan a specific workspace + +When you want to scope to one team's workspace — or to audit a public +workspace someone flagged in a bug bounty report — pass the workspace ID +or paste the URL straight from the browser: + +```bash +# By workspace UID +kingfisher scan postman \ + --workspace 11111111-2222-3333-4444-555555555555 + +# Or paste the web URL — Kingfisher extracts the UID +kingfisher scan postman \ + --workspace https://www.postman.com/team-handle/workspace/abc-uid-123 +``` + +Repeat the flag to scan multiple workspaces in one run. + +## Scan a single collection or environment + +For CI, you usually want to scan the specific collection that gets shared +with partners on every release, not the whole workspace: + +```bash +# Single collection — useful in CI on a known-shared collection +kingfisher scan postman \ + --collection 12345678-abcd-efgh-ijkl-mnopqrstuvwx + +# Single environment — useful when you suspect one env in particular +kingfisher scan postman \ + --environment 12345678-abcd-efgh-ijkl-mnopqrstuvwx +``` + +Both flags are repeatable. + +## What a finding looks like + +Findings come back tagged with the Postman web URL of the resource they +were found in. That makes triage one click — paste the URL into a browser +and you're looking at the exact collection or environment that needs +remediation: + +``` +GITHUB PERSONAL ACCESS TOKEN => [KINGFISHER.GITHUB.2] + |Finding.......: ghp_EZopZDMW... + |Confidence....: medium + |Validation....: Active + |Path..........: https://go.postman.co/environments/env-uid-1 +``` + +In JSON output, the URL appears in the finding's source/origin block, so +it round-trips into your triage tooling alongside the validation verdict. + +## The end-to-end response loop + +Combine Postman scanning with the rest of Kingfisher's response chain and +you have a complete incident workflow: + +```bash +# 1. Scan + validate + map blast radius across every workspace +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all \ + --access-map \ + --format json \ + --output postman-findings.json + +# 2. Pull just the live, high-blast-radius findings +jq '.findings + | map(select(.validation.status == "Active")) + | map(select(.access_map != null))' \ + postman-findings.json > urgent.json + +# 3. Revoke the most urgent ones in place — by rule +kingfisher revoke --rule github "$LEAKED_GITHUB_TOKEN" +kingfisher revoke --rule slack "$LEAKED_SLACK_TOKEN" +``` + +Find → prioritize → revoke, all without leaving the terminal. + +## Self-hosted and enterprise + +If your team runs Postman behind a corporate proxy or uses an enterprise +endpoint, override the API URL: + +```bash +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all \ + --api-url https://postman.internal.example.com/ +``` + +## Out of scope (so you can plan around it) + +- **Postman Vault secrets.** Vault values stay client-side and are not + reachable from the Postman API. If you've migrated everything sensitive + into Vault, those values are not in this scan's blast radius — by + design. Anything still in `type: secret` environment variables, however, + is fully exposed. +- **Postman API Network discovery.** Postman does not expose a public + search API for the API Network. If you want to scan a public workspace, + you have to hand Kingfisher its workspace ID. There is no `--query` + option that crawls all of Postman. +- **Postman request history.** Per-user, never API-accessible. + +## Why this matters + +Most secret scanners only see Postman content if a developer has manually +exported a collection JSON and committed it. That's the smallest fraction +of the actual exposure. The majority of leaked Postman credentials live in +the workspace itself: in a "secret" environment variable that someone set +six months ago, in a saved example response from a debugging session, in +a pre-request script that hard-codes an AWS keypair "just for now." + +By scanning the API directly, Kingfisher sees the same surface a +compromised Postman API key would see — which is exactly the surface that +matters from a defender's perspective. + +## Get started + +```bash +# Install — see the README for other platforms +brew install kingfisher + +# Scan +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan postman --all +``` + +If there's a Postman feature you want covered or you find a workflow that +doesn't fit, open an issue at +[mongodb/kingfisher](https://github.com/mongodb/kingfisher/issues). diff --git a/docs-site/docs/changelog.md b/docs-site/docs/changelog.md index 30e9c0d..b1812ad 100644 --- a/docs-site/docs/changelog.md +++ b/docs-site/docs/changelog.md @@ -8,6 +8,7 @@ description: "Kingfisher release history: new features, rules, bug fixes, and im All notable changes to this project will be documented in this file. ## [unreleased v1.98.0] +- 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. - Fixed [#359](https://github.com/mongodb/kingfisher/issues/359): added `kingfisher.github.9` to detect the new ~520-character stateless GitHub App installation token format (`ghs__`). The legacy 36-character `ghs_` rule (`kingfisher.github.5`) is retained for older / GHES-issued tokens that are still in circulation. - Added provider endpoint overrides for validation and revocation via global `--endpoint PROVIDER=URL` and `--endpoint-config FILE`, with built-in support for self-hosted GitHub, GitLab, Gitea, Jira, Confluence, and Artifactory instances. diff --git a/docs-site/docs/features/access-map.md b/docs-site/docs/features/access-map.md index a966001..7b08664 100644 --- a/docs-site/docs/features/access-map.md +++ b/docs-site/docs/features/access-map.md @@ -13,6 +13,11 @@ There are two ways to produce access maps: Kingfisher validates detected secrets and automatically generates access-map entries for supported credential types. - **Standalone**: `kingfisher access-map [credential_file]` This reads a credential artifact from disk and maps it directly. + Standalone access-map defaults to JSON output. The examples below use + `--format json` explicitly so the output type stays unambiguous when + redirecting to a file. Use `--format html` for a standalone HTML report, + and `--output ` if you prefer writing directly instead of using shell + redirection. > Access mapping runs additional network requests. Only use it when you are authorized to inspect the target account/workspace. @@ -27,8 +32,7 @@ flowchart LR Dispatch --> Provider[Provider mapper] Provider --> APIs[Provider APIs] APIs --> Result[AccessMapResult] - Result --> JSON[JSON stdout or file] - Result --> HTML[Optional HTML report] + Result --> Output[JSON or HTML output] ``` ### Scan-Time Flow @@ -82,7 +86,7 @@ Access map only runs for credential types Kingfisher knows how to authenticate w ```bash printf '%s' 'ghp_example...' > ./github.token -kingfisher access-map github ./github.token --json-out github.access-map.json +kingfisher access-map github ./github.token --format json > github.access-map.json ``` #### Notes (GitHub) @@ -99,7 +103,7 @@ kingfisher access-map github ./github.token --json-out github.access-map.json ```bash printf '%s' 'glpat-example...' > ./gitlab.token -kingfisher access-map gitlab ./gitlab.token --json-out gitlab.access-map.json +kingfisher access-map gitlab ./gitlab.token --format json > gitlab.access-map.json ``` #### Notes (GitLab) @@ -116,7 +120,7 @@ kingfisher access-map gitlab ./gitlab.token --json-out gitlab.access-map.json ```bash printf '%s' 'xoxp-example...' > ./slack.token -kingfisher access-map slack ./slack.token --json-out slack.access-map.json +kingfisher access-map slack ./slack.token --format json > slack.access-map.json ``` ### AWS (`aws`) @@ -143,7 +147,7 @@ cat > ./aws.json <<'EOF' } EOF -kingfisher access-map aws ./aws.json --json-out aws.access-map.json +kingfisher access-map aws ./aws.json --format json > aws.access-map.json ``` ```bash @@ -153,7 +157,7 @@ aws_secret_access_key=.... aws_session_token=.... EOF -kingfisher access-map aws ./aws.env --json-out aws.access-map.json +kingfisher access-map aws ./aws.env --format json > aws.access-map.json ``` Kingfisher performs read-only enumeration for the IAM principal and, when allowed by the credential, visible resources in several common AWS services including S3, EC2, IAM, Lambda, DynamoDB, KMS, Secrets Manager, SQS, SNS, RDS, ECR, and SSM Parameter Store. @@ -182,7 +186,7 @@ cat > ./alibaba.json <<'EOF' } EOF -kingfisher access-map alibaba ./alibaba.json --json-out alibaba.access-map.json +kingfisher access-map alibaba ./alibaba.json --format json > alibaba.access-map.json ``` ```bash @@ -192,7 +196,7 @@ access_key_secret=.... security_token=.... EOF -kingfisher access-map alibaba ./alibaba.env --json-out alibaba.access-map.json +kingfisher access-map alibaba ./alibaba.env --format json > alibaba.access-map.json ``` Kingfisher resolves the Alibaba Cloud caller identity with `sts:GetCallerIdentity` for both long-lived access key pairs and STS temporary credentials discovered during scanning. Current coverage is identity-focused: it maps the account and resolved RAM principal, and records that broader Alibaba service enumeration is not yet available. @@ -204,7 +208,7 @@ Kingfisher resolves the Alibaba Cloud caller identity with `sts:GetCallerIdentit #### Standalone example (GCP) ```bash -kingfisher access-map gcp ./service-account.json --json-out gcp.access-map.json +kingfisher access-map gcp ./service-account.json --format json > gcp.access-map.json ``` ### Azure Storage (`azure`) @@ -223,7 +227,7 @@ cat > ./azure-storage.json <<'EOF' } EOF -kingfisher access-map azure ./azure-storage.json --json-out azure.access-map.json +kingfisher access-map azure ./azure-storage.json --format json > azure.access-map.json ``` Kingfisher treats the account key as full-control Storage credentials and performs best-effort enumeration across Blob containers, File shares, and Queue resources reachable with that key. @@ -240,7 +244,7 @@ Azure DevOps access mapping is supported when a **validated Azure DevOps PAT** i ```bash printf '%s' 'postgres://user:pass@db.example.com:5432/mydb' > ./postgres.uri -kingfisher access-map postgres ./postgres.uri --json-out postgres.access-map.json +kingfisher access-map postgres ./postgres.uri --format json > postgres.access-map.json ``` ### MongoDB (`mongodb` / `mongo`) @@ -251,7 +255,7 @@ kingfisher access-map postgres ./postgres.uri --json-out postgres.access-map.jso ```bash printf '%s' 'mongodb+srv://user:pass@cluster.example.net/?retryWrites=true&w=majority' > ./mongodb.uri -kingfisher access-map mongodb ./mongodb.uri --json-out mongodb.access-map.json +kingfisher access-map mongodb ./mongodb.uri --format json > mongodb.access-map.json ``` ### Hugging Face (`huggingface` / `hf`) @@ -267,7 +271,7 @@ Kingfisher queries the `/api/whoami-v2` endpoint to resolve the token identity, ```bash printf '%s' 'hf_example...' > ./huggingface.token -kingfisher access-map huggingface ./huggingface.token --json-out huggingface.access-map.json +kingfisher access-map huggingface ./huggingface.token --format json > huggingface.access-map.json ``` #### Notes (Hugging Face) @@ -286,7 +290,7 @@ Kingfisher queries `/api/v1/user` for identity, enumerates organizations via `/a ```bash printf '%s' 'your_gitea_pat...' > ./gitea.token -kingfisher access-map gitea ./gitea.token --json-out gitea.access-map.json +kingfisher access-map gitea ./gitea.token --format json > gitea.access-map.json ``` #### Notes (Gitea) @@ -305,7 +309,7 @@ Kingfisher queries `/2.0/user` for identity, enumerates workspace memberships an ```bash printf '%s' 'your_bitbucket_token...' > ./bitbucket.token -kingfisher access-map bitbucket ./bitbucket.token --json-out bitbucket.access-map.json +kingfisher access-map bitbucket ./bitbucket.token --format json > bitbucket.access-map.json ``` #### Notes (Bitbucket) @@ -324,7 +328,7 @@ Kingfisher queries `/v2/access-token` for token metadata and scopes, `/v2/user` ```bash printf '%s' 'bkua_example...' > ./buildkite.token -kingfisher access-map buildkite ./buildkite.token --json-out buildkite.access-map.json +kingfisher access-map buildkite ./buildkite.token --format json > buildkite.access-map.json ``` #### Notes (Buildkite) @@ -348,7 +352,7 @@ If organizations/projects are not enumerable (scope-limited keys), Kingfisher st ```bash printf '%s' 'pat.example...' > ./harness.token -kingfisher access-map harness ./harness.token --json-out harness.access-map.json +kingfisher access-map harness ./harness.token --format json > harness.access-map.json ``` #### Notes (Harness) @@ -373,7 +377,7 @@ Kingfisher performs read-only scope probing and best-effort resource enumeration ```bash printf '%s' 'sk-example...' > ./openai.token -kingfisher access-map openai ./openai.token --json-out openai.access-map.json +kingfisher access-map openai ./openai.token --format json > openai.access-map.json ``` #### Notes (OpenAI) @@ -395,7 +399,7 @@ Kingfisher performs read-only enumeration via: ```bash printf '%s' 'sk-ant-api-example...' > ./anthropic.token -kingfisher access-map anthropic ./anthropic.token --json-out anthropic.access-map.json +kingfisher access-map anthropic ./anthropic.token --format json > anthropic.access-map.json ``` #### Notes (Anthropic) @@ -430,7 +434,7 @@ cat > ./salesforce.json <<'EOF' } EOF -kingfisher access-map salesforce ./salesforce.json --json-out salesforce.access-map.json +kingfisher access-map salesforce ./salesforce.json --format json > salesforce.access-map.json ``` #### Notes (Salesforce) @@ -452,7 +456,7 @@ Kingfisher performs read-only identity resolution via: ```bash printf '%s' 'wandb_v1_example...' > ./wandb.token -kingfisher access-map weightsandbiases ./wandb.token --json-out wandb.access-map.json +kingfisher access-map weightsandbiases ./wandb.token --format json > wandb.access-map.json ``` #### Notes (Weights & Biases) @@ -473,7 +477,7 @@ Kingfisher parses the webhook URL to extract the tenant ID and webhook identity, ```bash printf '%s' 'https://contoso.webhook.office.com/webhookb2/...' > ./teams.webhook -kingfisher access-map microsoftteams ./teams.webhook --json-out teams.access-map.json +kingfisher access-map microsoftteams ./teams.webhook --format json > teams.access-map.json ``` #### Notes (Microsoft Teams) @@ -499,7 +503,7 @@ Severity is Critical for account administrators, High for standard members with ```bash printf '%s' 'eyJhbGciOi...' > ./monday.token -kingfisher access-map monday ./monday.token --json-out monday.access-map.json +kingfisher access-map monday ./monday.token --format json > monday.access-map.json ``` #### Notes (monday.com) @@ -529,7 +533,7 @@ Severity is High when the token reaches an organization workspace with more than ```bash printf '%s' '2/12345.../abcdef...' > ./asana.token -kingfisher access-map asana ./asana.token --json-out asana.access-map.json +kingfisher access-map asana ./asana.token --format json > asana.access-map.json ``` #### Notes (Asana) diff --git a/docs-site/docs/usage/basic-scanning.md b/docs-site/docs/usage/basic-scanning.md index 218b36d..b64d837 100644 --- a/docs-site/docs/usage/basic-scanning.md +++ b/docs-site/docs/usage/basic-scanning.md @@ -519,12 +519,11 @@ kingfisher scan ./my-project \ ## Scanning Platform-Specific Targets > **Deprecated** -> Legacy scan flags such as `--github-user`, `--gitlab-group`, -> `--bitbucket-workspace`, `--azure-organization`, `--huggingface-user`, +> Older documentation may refer to legacy provider flags such as +> `--github-user`, `--gitlab-group`, `--bitbucket-workspace`, > `--slack-query`, `--jira-url`, `--confluence-url`, `--s3-bucket`, -> `--gcs-bucket`, and `--docker-image` still work for now, but they trigger a -> warning and will be removed in a future release. Migrate to the -> `kingfisher scan ` subcommands below to future-proof your automations. +> `--gcs-bucket`, and `--docker-image`. Use the +> `kingfisher scan ` subcommands below instead. --- @@ -766,10 +765,10 @@ kingfisher scan gitlab --group my-group --gitlab-exclude my-group/**/legacy-* -- ### Scan Azure Repos organization or collection (requires `KF_AZURE_TOKEN` or `KF_AZURE_PAT`) ```bash -kingfisher scan azure --organization my-org +kingfisher scan azure --azure-organization my-org # Azure Repos Server example -KF_AZURE_PAT="pat" kingfisher scan azure --organization DefaultCollection --azure-base-url https://ado.internal.example/tfs/ +KF_AZURE_PAT="pat" kingfisher scan azure --azure-organization DefaultCollection --base-url https://ado.internal.example/tfs/ ``` ### Scan specific Azure Repos projects @@ -777,8 +776,8 @@ KF_AZURE_PAT="pat" kingfisher scan azure --organization DefaultCollection --azur Projects are specified as `ORGANIZATION/PROJECT`. Repeat the flag for multiple projects. ```bash -kingfisher scan azure --project my-org/payments \ - --project my-org/core-platform +kingfisher scan azure --azure-project my-org/payments \ + --azure-project my-org/core-platform ``` ### Skip specific Azure repositories during enumeration @@ -786,7 +785,7 @@ kingfisher scan azure --project my-org/payments \ Repeat `--azure-exclude` to ignore repositories when scanning organizations or projects. Use identifiers like `ORGANIZATION/PROJECT/REPOSITORY`. Repositories that share the same name as their project can be excluded with `ORGANIZATION/PROJECT`, and gitignore-style patterns such as `my-org/*/archive-*` are also supported. ```bash -kingfisher scan azure --organization my-org \ +kingfisher scan azure --azure-organization my-org \ --azure-exclude my-org/payments/legacy-service \ --azure-exclude my-org/**/archive-* ``` @@ -794,11 +793,11 @@ kingfisher scan azure --organization my-org \ ### List Azure repositories ```bash -kingfisher scan azure --organization my-org --list-only +kingfisher scan azure --azure-organization my-org --list-only # list repositories for specific projects -kingfisher scan azure --project my-org/app --project my-org/api --list-only +kingfisher scan azure --azure-project my-org/app --azure-project my-org/api --list-only # skip specific repositories while listing (supports glob patterns) -kingfisher scan azure --organization my-org --azure-exclude my-org/**/experimental-* --list-only +kingfisher scan azure --azure-organization my-org --azure-exclude my-org/**/experimental-* --list-only ``` --- @@ -810,7 +809,7 @@ kingfisher scan azure --organization my-org --azure-exclude my-org/**/experiment ```bash kingfisher scan gitea --organization my-org # self-hosted example -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --organization platform --gitea-api-url https://gitea.internal.example/api/v1/ +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --organization platform --api-url https://gitea.internal.example/api/v1/ ``` ### Scan Gitea user @@ -847,9 +846,9 @@ KF_GITEA_TOKEN="gtoken" KF_GITEA_USERNAME="org" \ ```bash kingfisher scan gitea --organization my-org --list-only # enumerate every organization visible to the authenticated user -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --all-gitea-organizations --list-only +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --all-organizations --list-only # self-hosted example -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --user johndoe --gitea-api-url https://gitea.internal.example/api/v1/ --list-only +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --user johndoe --api-url https://gitea.internal.example/api/v1/ --list-only ``` --- @@ -922,7 +921,7 @@ Bitbucket no longer supports App Tokens as of September 9, 2025: https://support ### Self-hosted Bitbucket Server -Use `--bitbucket-api-url` to point Kingfisher at your server's REST endpoint, for example `https://bitbucket.example.com/rest/api/1.0/`. Provide credentials with `KF_BITBUCKET_USERNAME` plus either `KF_BITBUCKET_TOKEN` or `KF_BITBUCKET_PASSWORD`, and pass `--tls-mode=off` (or the legacy `--ignore-certs`) when connecting to HTTP or otherwise insecure instances. +Use `--api-url` to point Kingfisher at your server's REST endpoint, for example `https://bitbucket.example.com/rest/api/1.0/`. Provide credentials with `KF_BITBUCKET_USERNAME` plus either `KF_BITBUCKET_TOKEN` or `KF_BITBUCKET_PASSWORD`, and pass `--tls-mode=off` (or the legacy `--ignore-certs`) when connecting to HTTP or otherwise insecure instances. --- @@ -933,13 +932,13 @@ Hugging Face hosts git repositories for models, datasets, and Spaces. Kingfisher ### Scan Hugging Face user ```bash -kingfisher scan huggingface --user +kingfisher scan huggingface --huggingface-user ``` ### Scan Hugging Face organization ```bash -kingfisher scan huggingface --organization +kingfisher scan huggingface --huggingface-organization ``` ### Scan specific Hugging Face resources @@ -947,9 +946,9 @@ kingfisher scan huggingface --organization Scan individual repositories by ID (owner/name) or by passing the full HTTPS URL: ```bash -kingfisher scan huggingface --model -kingfisher scan huggingface --dataset https://huggingface.co/datasets// -kingfisher scan huggingface --space +kingfisher scan huggingface --huggingface-model +kingfisher scan huggingface --huggingface-dataset https://huggingface.co/datasets// +kingfisher scan huggingface --huggingface-space ``` Use `--huggingface-exclude` to omit results returned by user or organization enumeration. Prefix values with `model:`, `dataset:`, or `space:` when you only want to skip a specific resource type. @@ -957,7 +956,7 @@ Use `--huggingface-exclude` to omit results returned by user or organization enu ### List Hugging Face repositories ```bash -kingfisher scan huggingface --user --list-only +kingfisher scan huggingface --huggingface-user --list-only ``` ### Authenticate to Hugging Face @@ -1015,7 +1014,7 @@ KF_CONFLUENCE_USER="user@example.com" KF_CONFLUENCE_TOKEN="token" \ --max-results 500 ``` -Use the base URL of your Confluence site for `--confluence-url`. Kingfisher automatically adds `/rest/api` to the end, so `https://example.com/wiki` and `https://example.com` both work depending on your server configuration. +Use the base URL of your Confluence site for `--url`. Kingfisher automatically adds `/rest/api` to the end, so `https://example.com/wiki` and `https://example.com` both work depending on your server configuration. Generate a personal access token and set it in the `KF_CONFLUENCE_TOKEN` environment variable. By default, Kingfisher sends the token as a bearer token in the `Authorization` header. diff --git a/docs-site/docs/usage/integrations.md b/docs-site/docs/usage/integrations.md index bdc5d21..4a5040d 100644 --- a/docs-site/docs/usage/integrations.md +++ b/docs-site/docs/usage/integrations.md @@ -22,6 +22,7 @@ This guide covers how to scan various platforms and services with Kingfisher. - [Confluence](#confluence) - [Slack](#slack) - [Microsoft Teams](#microsoft-teams) +- [Postman](#postman) - [Environment Variables](#environment-variables) ## AWS S3 @@ -130,12 +131,11 @@ kingfisher scan docker private.registry.example.com/my-image:tag ``` > **Deprecated** -> Legacy scan flags such as `--github-user`, `--gitlab-group`, -> `--bitbucket-workspace`, `--azure-organization`, `--huggingface-user`, +> Older documentation may refer to legacy provider flags such as +> `--github-user`, `--gitlab-group`, `--bitbucket-workspace`, > `--slack-query`, `--jira-url`, `--confluence-url`, `--s3-bucket`, -> `--gcs-bucket`, and `--docker-image` still work for now, but they trigger a -> warning and will be removed in a future release. Migrate to the -> `kingfisher scan ` subcommands below to future-proof your automations. +> `--gcs-bucket`, and `--docker-image`. Use the +> `kingfisher scan ` subcommands below instead. ## GitHub @@ -297,10 +297,10 @@ kingfisher scan gitlab --group my-group --gitlab-exclude my-group/**/legacy-* -- ### Scan Azure Repos organization or collection (requires `KF_AZURE_TOKEN` or `KF_AZURE_PAT`) ```bash -kingfisher scan azure --organization my-org +kingfisher scan azure --azure-organization my-org # Azure Repos Server example -KF_AZURE_PAT="pat" kingfisher scan azure --organization DefaultCollection --azure-base-url https://ado.internal.example/tfs/ +KF_AZURE_PAT="pat" kingfisher scan azure --azure-organization DefaultCollection --base-url https://ado.internal.example/tfs/ ``` ### Scan specific Azure Repos projects @@ -308,8 +308,8 @@ KF_AZURE_PAT="pat" kingfisher scan azure --organization DefaultCollection --azur Projects are specified as `ORGANIZATION/PROJECT`. Repeat the flag for multiple projects. ```bash -kingfisher scan azure --project my-org/payments \ - --project my-org/core-platform +kingfisher scan azure --azure-project my-org/payments \ + --azure-project my-org/core-platform ``` ### Skip specific Azure repositories during enumeration @@ -320,7 +320,7 @@ name as their project can be excluded with `ORGANIZATION/PROJECT`, and gitignore patterns such as `my-org/*/archive-*` are also supported. ```bash -kingfisher scan azure --organization my-org \ +kingfisher scan azure --azure-organization my-org \ --azure-exclude my-org/payments/legacy-service \ --azure-exclude my-org/**/archive-* ``` @@ -328,11 +328,11 @@ kingfisher scan azure --organization my-org \ ### List Azure repositories ```bash -kingfisher scan azure --organization my-org --list-only +kingfisher scan azure --azure-organization my-org --list-only # list repositories for specific projects -kingfisher scan azure --project my-org/app --project my-org/api --list-only +kingfisher scan azure --azure-project my-org/app --azure-project my-org/api --list-only # skip specific repositories while listing (supports glob patterns) -kingfisher scan azure --organization my-org --azure-exclude my-org/**/experimental-* --list-only +kingfisher scan azure --azure-organization my-org --azure-exclude my-org/**/experimental-* --list-only ``` ## Gitea @@ -342,7 +342,7 @@ kingfisher scan azure --organization my-org --azure-exclude my-org/**/experiment ```bash kingfisher scan gitea --organization my-org # self-hosted example -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --organization platform --gitea-api-url https://gitea.internal.example/api/v1/ +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --organization platform --api-url https://gitea.internal.example/api/v1/ ``` ### Scan Gitea user @@ -383,9 +383,9 @@ KF_GITEA_TOKEN="gtoken" KF_GITEA_USERNAME="org" \ ```bash kingfisher scan gitea --organization my-org --list-only # enumerate every organization visible to the authenticated user -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --all-gitea-organizations --list-only +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --all-organizations --list-only # self-hosted example -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --user johndoe --gitea-api-url https://gitea.internal.example/api/v1/ --list-only +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --user johndoe --api-url https://gitea.internal.example/api/v1/ --list-only ``` ## Bitbucket @@ -464,7 +464,7 @@ https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/ ### Self-hosted Bitbucket Server -Use `--bitbucket-api-url` to point Kingfisher at your server's REST endpoint, for example +Use `--api-url` to point Kingfisher at your server's REST endpoint, for example `https://bitbucket.example.com/rest/api/1.0/`. Provide credentials with `KF_BITBUCKET_USERNAME` plus either `KF_BITBUCKET_TOKEN` or `KF_BITBUCKET_PASSWORD`, and pass `--ignore-certs` when connecting to HTTP or otherwise insecure instances. @@ -476,13 +476,13 @@ Hugging Face hosts git repositories for models, datasets, and Spaces. Kingfisher ### Scan Hugging Face user ```bash -kingfisher scan huggingface --user +kingfisher scan huggingface --huggingface-user ``` ### Scan Hugging Face organization ```bash -kingfisher scan huggingface --organization +kingfisher scan huggingface --huggingface-organization ``` ### Scan specific Hugging Face resources @@ -490,9 +490,9 @@ kingfisher scan huggingface --organization Scan individual repositories by ID (owner/name) or by passing the full HTTPS URL: ```bash -kingfisher scan huggingface --model -kingfisher scan huggingface --dataset https://huggingface.co/datasets// -kingfisher scan huggingface --space +kingfisher scan huggingface --huggingface-model +kingfisher scan huggingface --huggingface-dataset https://huggingface.co/datasets// +kingfisher scan huggingface --huggingface-space ``` Use `--huggingface-exclude` to omit results returned by user or organization enumeration. Prefix values with `model:`, `dataset:`, or `space:` when you only want to skip a specific resource type. @@ -500,7 +500,7 @@ Use `--huggingface-exclude` to omit results returned by user or organization enu ### List Hugging Face repositories ```bash -kingfisher scan huggingface --user --list-only +kingfisher scan huggingface --huggingface-user --list-only ``` ### Authenticate to Hugging Face @@ -554,7 +554,7 @@ KF_CONFLUENCE_USER="user@example.com" KF_CONFLUENCE_TOKEN="token" \ --max-results 500 ``` -Use the base URL of your Confluence site for `--confluence-url`. Kingfisher +Use the base URL of your Confluence site for `--url`. Kingfisher automatically adds `/rest/api` to the end, so `https://example.com/wiki` and `https://example.com` both work depending on your server configuration. @@ -601,6 +601,61 @@ The token must be a Microsoft Graph API access token with `ChannelMessage.Read.A - Client credentials flow for application permissions - Authorization code flow for delegated permissions +## Postman + +Kingfisher fetches Postman workspaces, collections, and environments via the public Postman API (`https://api.getpostman.com/`) and scans the JSON for hard-coded credentials. The most common high-value findings are: + +- Bearer tokens, API keys, and basic-auth passwords inside collection request `auth` blocks +- Hard-coded credentials inside pre-request and test scripts +- Saved example responses that echo tokens +- **"Secret"-typed environment variables** — Postman's `secret` flag is a UI-mask only, the API returns the plaintext value to any read-capable key + +### Scan every workspace, collection, and environment visible to the API key + +```bash +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 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-environment 12345678-abcd-efgh-ijkl-mnopqrstuvwx +``` + +### Include mocks and monitors + +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 +``` + +### Self-hosted / enterprise endpoint + +```bash +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan --postman-all \ + --postman-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. + ## Environment Variables | Variable | Purpose | @@ -621,6 +676,7 @@ The token must be a Microsoft Graph API access token with `ChannelMessage.Read.A | `KF_CONFLUENCE_TOKEN` | Confluence API token | | `KF_SLACK_TOKEN` | Slack API token | | `KF_TEAMS_TOKEN` | Microsoft Graph API token for Teams message search | +| `KF_POSTMAN_TOKEN` / `POSTMAN_API_KEY` | Postman API key (sent as `X-Api-Key`) for workspace, collection, and environment scanning | | `KF_DOCKER_TOKEN` | Docker registry token (`user:pass` or bearer token). If unset, credentials from the Docker keychain are used | | `KF_AWS_KEY`, `KF_AWS_SECRET`, and `KF_AWS_SESSION_TOKEN` | AWS credentials for S3 bucket scanning. Session token is optional, for temporary credentials | diff --git a/docs/ACCESS_MAP.md b/docs/ACCESS_MAP.md index 1ccaada..3a70d83 100644 --- a/docs/ACCESS_MAP.md +++ b/docs/ACCESS_MAP.md @@ -8,6 +8,11 @@ There are two ways to produce access maps: Kingfisher validates detected secrets and automatically generates access-map entries for supported credential types. - **Standalone**: `kingfisher access-map [credential_file]` This reads a credential artifact from disk and maps it directly. + Standalone access-map defaults to JSON output. The examples below use + `--format json` explicitly so the output type stays unambiguous when + redirecting to a file. Use `--format html` for a standalone HTML report, + and `--output ` if you prefer writing directly instead of using shell + redirection. > Access mapping runs additional network requests. Only use it when you are authorized to inspect the target account/workspace. @@ -22,8 +27,7 @@ flowchart LR Dispatch --> Provider[Provider mapper] Provider --> APIs[Provider APIs] APIs --> Result[AccessMapResult] - Result --> JSON[JSON stdout or file] - Result --> HTML[Optional HTML report] + Result --> Output[JSON or HTML output] ``` ### Scan-Time Flow @@ -77,7 +81,7 @@ Access map only runs for credential types Kingfisher knows how to authenticate w ```bash printf '%s' 'ghp_example...' > ./github.token -kingfisher access-map github ./github.token --json-out github.access-map.json +kingfisher access-map github ./github.token --format json > github.access-map.json ``` #### Notes (GitHub) @@ -94,7 +98,7 @@ kingfisher access-map github ./github.token --json-out github.access-map.json ```bash printf '%s' 'glpat-example...' > ./gitlab.token -kingfisher access-map gitlab ./gitlab.token --json-out gitlab.access-map.json +kingfisher access-map gitlab ./gitlab.token --format json > gitlab.access-map.json ``` #### Notes (GitLab) @@ -111,7 +115,7 @@ kingfisher access-map gitlab ./gitlab.token --json-out gitlab.access-map.json ```bash printf '%s' 'xoxp-example...' > ./slack.token -kingfisher access-map slack ./slack.token --json-out slack.access-map.json +kingfisher access-map slack ./slack.token --format json > slack.access-map.json ``` ### AWS (`aws`) @@ -138,7 +142,7 @@ cat > ./aws.json <<'EOF' } EOF -kingfisher access-map aws ./aws.json --json-out aws.access-map.json +kingfisher access-map aws ./aws.json --format json > aws.access-map.json ``` ```bash @@ -148,7 +152,7 @@ aws_secret_access_key=.... aws_session_token=.... EOF -kingfisher access-map aws ./aws.env --json-out aws.access-map.json +kingfisher access-map aws ./aws.env --format json > aws.access-map.json ``` Kingfisher performs read-only enumeration for the IAM principal and, when allowed by the credential, visible resources in several common AWS services including S3, EC2, IAM, Lambda, DynamoDB, KMS, Secrets Manager, SQS, SNS, RDS, ECR, and SSM Parameter Store. @@ -177,7 +181,7 @@ cat > ./alibaba.json <<'EOF' } EOF -kingfisher access-map alibaba ./alibaba.json --json-out alibaba.access-map.json +kingfisher access-map alibaba ./alibaba.json --format json > alibaba.access-map.json ``` ```bash @@ -187,7 +191,7 @@ access_key_secret=.... security_token=.... EOF -kingfisher access-map alibaba ./alibaba.env --json-out alibaba.access-map.json +kingfisher access-map alibaba ./alibaba.env --format json > alibaba.access-map.json ``` Kingfisher resolves the Alibaba Cloud caller identity with `sts:GetCallerIdentity` for both long-lived access key pairs and STS temporary credentials discovered during scanning. Current coverage is identity-focused: it maps the account and resolved RAM principal, and records that broader Alibaba service enumeration is not yet available. @@ -199,7 +203,7 @@ Kingfisher resolves the Alibaba Cloud caller identity with `sts:GetCallerIdentit #### Standalone example (GCP) ```bash -kingfisher access-map gcp ./service-account.json --json-out gcp.access-map.json +kingfisher access-map gcp ./service-account.json --format json > gcp.access-map.json ``` ### Azure Storage (`azure`) @@ -218,7 +222,7 @@ cat > ./azure-storage.json <<'EOF' } EOF -kingfisher access-map azure ./azure-storage.json --json-out azure.access-map.json +kingfisher access-map azure ./azure-storage.json --format json > azure.access-map.json ``` Kingfisher treats the account key as full-control Storage credentials and performs best-effort enumeration across Blob containers, File shares, and Queue resources reachable with that key. @@ -235,7 +239,7 @@ Azure DevOps access mapping is supported when a **validated Azure DevOps PAT** i ```bash printf '%s' 'postgres://user:pass@db.example.com:5432/mydb' > ./postgres.uri -kingfisher access-map postgres ./postgres.uri --json-out postgres.access-map.json +kingfisher access-map postgres ./postgres.uri --format json > postgres.access-map.json ``` ### MongoDB (`mongodb` / `mongo`) @@ -246,7 +250,7 @@ kingfisher access-map postgres ./postgres.uri --json-out postgres.access-map.jso ```bash printf '%s' 'mongodb+srv://user:pass@cluster.example.net/?retryWrites=true&w=majority' > ./mongodb.uri -kingfisher access-map mongodb ./mongodb.uri --json-out mongodb.access-map.json +kingfisher access-map mongodb ./mongodb.uri --format json > mongodb.access-map.json ``` ### Hugging Face (`huggingface` / `hf`) @@ -262,7 +266,7 @@ Kingfisher queries the `/api/whoami-v2` endpoint to resolve the token identity, ```bash printf '%s' 'hf_example...' > ./huggingface.token -kingfisher access-map huggingface ./huggingface.token --json-out huggingface.access-map.json +kingfisher access-map huggingface ./huggingface.token --format json > huggingface.access-map.json ``` #### Notes (Hugging Face) @@ -281,7 +285,7 @@ Kingfisher queries `/api/v1/user` for identity, enumerates organizations via `/a ```bash printf '%s' 'your_gitea_pat...' > ./gitea.token -kingfisher access-map gitea ./gitea.token --json-out gitea.access-map.json +kingfisher access-map gitea ./gitea.token --format json > gitea.access-map.json ``` #### Notes (Gitea) @@ -300,7 +304,7 @@ Kingfisher queries `/2.0/user` for identity, enumerates workspace memberships an ```bash printf '%s' 'your_bitbucket_token...' > ./bitbucket.token -kingfisher access-map bitbucket ./bitbucket.token --json-out bitbucket.access-map.json +kingfisher access-map bitbucket ./bitbucket.token --format json > bitbucket.access-map.json ``` #### Notes (Bitbucket) @@ -319,7 +323,7 @@ Kingfisher queries `/v2/access-token` for token metadata and scopes, `/v2/user` ```bash printf '%s' 'bkua_example...' > ./buildkite.token -kingfisher access-map buildkite ./buildkite.token --json-out buildkite.access-map.json +kingfisher access-map buildkite ./buildkite.token --format json > buildkite.access-map.json ``` #### Notes (Buildkite) @@ -343,7 +347,7 @@ If organizations/projects are not enumerable (scope-limited keys), Kingfisher st ```bash printf '%s' 'pat.example...' > ./harness.token -kingfisher access-map harness ./harness.token --json-out harness.access-map.json +kingfisher access-map harness ./harness.token --format json > harness.access-map.json ``` #### Notes (Harness) @@ -368,7 +372,7 @@ Kingfisher performs read-only scope probing and best-effort resource enumeration ```bash printf '%s' 'sk-example...' > ./openai.token -kingfisher access-map openai ./openai.token --json-out openai.access-map.json +kingfisher access-map openai ./openai.token --format json > openai.access-map.json ``` #### Notes (OpenAI) @@ -390,7 +394,7 @@ Kingfisher performs read-only enumeration via: ```bash printf '%s' 'sk-ant-api-example...' > ./anthropic.token -kingfisher access-map anthropic ./anthropic.token --json-out anthropic.access-map.json +kingfisher access-map anthropic ./anthropic.token --format json > anthropic.access-map.json ``` #### Notes (Anthropic) @@ -425,7 +429,7 @@ cat > ./salesforce.json <<'EOF' } EOF -kingfisher access-map salesforce ./salesforce.json --json-out salesforce.access-map.json +kingfisher access-map salesforce ./salesforce.json --format json > salesforce.access-map.json ``` #### Notes (Salesforce) @@ -447,7 +451,7 @@ Kingfisher performs read-only identity resolution via: ```bash printf '%s' 'wandb_v1_example...' > ./wandb.token -kingfisher access-map weightsandbiases ./wandb.token --json-out wandb.access-map.json +kingfisher access-map weightsandbiases ./wandb.token --format json > wandb.access-map.json ``` #### Notes (Weights & Biases) @@ -468,7 +472,7 @@ Kingfisher parses the webhook URL to extract the tenant ID and webhook identity, ```bash printf '%s' 'https://contoso.webhook.office.com/webhookb2/...' > ./teams.webhook -kingfisher access-map microsoftteams ./teams.webhook --json-out teams.access-map.json +kingfisher access-map microsoftteams ./teams.webhook --format json > teams.access-map.json ``` #### Notes (Microsoft Teams) @@ -494,7 +498,7 @@ Severity is Critical for account administrators, High for standard members with ```bash printf '%s' 'eyJhbGciOi...' > ./monday.token -kingfisher access-map monday ./monday.token --json-out monday.access-map.json +kingfisher access-map monday ./monday.token --format json > monday.access-map.json ``` #### Notes (monday.com) @@ -524,7 +528,7 @@ Severity is High when the token reaches an organization workspace with more than ```bash printf '%s' '2/12345.../abcdef...' > ./asana.token -kingfisher access-map asana ./asana.token --json-out asana.access-map.json +kingfisher access-map asana ./asana.token --format json > asana.access-map.json ``` #### Notes (Asana) diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md index be142f1..a64e11b 100644 --- a/docs/INTEGRATIONS.md +++ b/docs/INTEGRATIONS.md @@ -19,6 +19,7 @@ This guide covers how to scan various platforms and services with Kingfisher. - [Confluence](#confluence) - [Slack](#slack) - [Microsoft Teams](#microsoft-teams) +- [Postman](#postman) - [Environment Variables](#environment-variables) ## AWS S3 @@ -127,12 +128,11 @@ kingfisher scan docker private.registry.example.com/my-image:tag ``` > **Deprecated** -> Legacy scan flags such as `--github-user`, `--gitlab-group`, -> `--bitbucket-workspace`, `--azure-organization`, `--huggingface-user`, +> Older documentation may refer to legacy provider flags such as +> `--github-user`, `--gitlab-group`, `--bitbucket-workspace`, > `--slack-query`, `--jira-url`, `--confluence-url`, `--s3-bucket`, -> `--gcs-bucket`, and `--docker-image` still work for now, but they trigger a -> warning and will be removed in a future release. Migrate to the -> `kingfisher scan ` subcommands below to future-proof your automations. +> `--gcs-bucket`, and `--docker-image`. Use the +> `kingfisher scan ` subcommands below instead. ## GitHub @@ -294,10 +294,10 @@ kingfisher scan gitlab --group my-group --gitlab-exclude my-group/**/legacy-* -- ### Scan Azure Repos organization or collection (requires `KF_AZURE_TOKEN` or `KF_AZURE_PAT`) ```bash -kingfisher scan azure --organization my-org +kingfisher scan azure --azure-organization my-org # Azure Repos Server example -KF_AZURE_PAT="pat" kingfisher scan azure --organization DefaultCollection --azure-base-url https://ado.internal.example/tfs/ +KF_AZURE_PAT="pat" kingfisher scan azure --azure-organization DefaultCollection --base-url https://ado.internal.example/tfs/ ``` ### Scan specific Azure Repos projects @@ -305,8 +305,8 @@ KF_AZURE_PAT="pat" kingfisher scan azure --organization DefaultCollection --azur Projects are specified as `ORGANIZATION/PROJECT`. Repeat the flag for multiple projects. ```bash -kingfisher scan azure --project my-org/payments \ - --project my-org/core-platform +kingfisher scan azure --azure-project my-org/payments \ + --azure-project my-org/core-platform ``` ### Skip specific Azure repositories during enumeration @@ -317,7 +317,7 @@ name as their project can be excluded with `ORGANIZATION/PROJECT`, and gitignore patterns such as `my-org/*/archive-*` are also supported. ```bash -kingfisher scan azure --organization my-org \ +kingfisher scan azure --azure-organization my-org \ --azure-exclude my-org/payments/legacy-service \ --azure-exclude my-org/**/archive-* ``` @@ -325,11 +325,11 @@ kingfisher scan azure --organization my-org \ ### List Azure repositories ```bash -kingfisher scan azure --organization my-org --list-only +kingfisher scan azure --azure-organization my-org --list-only # list repositories for specific projects -kingfisher scan azure --project my-org/app --project my-org/api --list-only +kingfisher scan azure --azure-project my-org/app --azure-project my-org/api --list-only # skip specific repositories while listing (supports glob patterns) -kingfisher scan azure --organization my-org --azure-exclude my-org/**/experimental-* --list-only +kingfisher scan azure --azure-organization my-org --azure-exclude my-org/**/experimental-* --list-only ``` ## Gitea @@ -339,7 +339,7 @@ kingfisher scan azure --organization my-org --azure-exclude my-org/**/experiment ```bash kingfisher scan gitea --organization my-org # self-hosted example -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --organization platform --gitea-api-url https://gitea.internal.example/api/v1/ +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --organization platform --api-url https://gitea.internal.example/api/v1/ ``` ### Scan Gitea user @@ -380,9 +380,9 @@ KF_GITEA_TOKEN="gtoken" KF_GITEA_USERNAME="org" \ ```bash kingfisher scan gitea --organization my-org --list-only # enumerate every organization visible to the authenticated user -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --all-gitea-organizations --list-only +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --all-organizations --list-only # self-hosted example -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --user johndoe --gitea-api-url https://gitea.internal.example/api/v1/ --list-only +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --user johndoe --api-url https://gitea.internal.example/api/v1/ --list-only ``` ## Bitbucket @@ -461,7 +461,7 @@ https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/ ### Self-hosted Bitbucket Server -Use `--bitbucket-api-url` to point Kingfisher at your server's REST endpoint, for example +Use `--api-url` to point Kingfisher at your server's REST endpoint, for example `https://bitbucket.example.com/rest/api/1.0/`. Provide credentials with `KF_BITBUCKET_USERNAME` plus either `KF_BITBUCKET_TOKEN` or `KF_BITBUCKET_PASSWORD`, and pass `--ignore-certs` when connecting to HTTP or otherwise insecure instances. @@ -473,13 +473,13 @@ Hugging Face hosts git repositories for models, datasets, and Spaces. Kingfisher ### Scan Hugging Face user ```bash -kingfisher scan huggingface --user +kingfisher scan huggingface --huggingface-user ``` ### Scan Hugging Face organization ```bash -kingfisher scan huggingface --organization +kingfisher scan huggingface --huggingface-organization ``` ### Scan specific Hugging Face resources @@ -487,9 +487,9 @@ kingfisher scan huggingface --organization Scan individual repositories by ID (owner/name) or by passing the full HTTPS URL: ```bash -kingfisher scan huggingface --model -kingfisher scan huggingface --dataset https://huggingface.co/datasets// -kingfisher scan huggingface --space +kingfisher scan huggingface --huggingface-model +kingfisher scan huggingface --huggingface-dataset https://huggingface.co/datasets// +kingfisher scan huggingface --huggingface-space ``` Use `--huggingface-exclude` to omit results returned by user or organization enumeration. Prefix values with `model:`, `dataset:`, or `space:` when you only want to skip a specific resource type. @@ -497,7 +497,7 @@ Use `--huggingface-exclude` to omit results returned by user or organization enu ### List Hugging Face repositories ```bash -kingfisher scan huggingface --user --list-only +kingfisher scan huggingface --huggingface-user --list-only ``` ### Authenticate to Hugging Face @@ -551,7 +551,7 @@ KF_CONFLUENCE_USER="user@example.com" KF_CONFLUENCE_TOKEN="token" \ --max-results 500 ``` -Use the base URL of your Confluence site for `--confluence-url`. Kingfisher +Use the base URL of your Confluence site for `--url`. Kingfisher automatically adds `/rest/api` to the end, so `https://example.com/wiki` and `https://example.com` both work depending on your server configuration. @@ -598,6 +598,61 @@ The token must be a Microsoft Graph API access token with `ChannelMessage.Read.A - Client credentials flow for application permissions - Authorization code flow for delegated permissions +## Postman + +Kingfisher fetches Postman workspaces, collections, and environments via the public Postman API (`https://api.getpostman.com/`) and scans the JSON for hard-coded credentials. The most common high-value findings are: + +- Bearer tokens, API keys, and basic-auth passwords inside collection request `auth` blocks +- Hard-coded credentials inside pre-request and test scripts +- Saved example responses that echo tokens +- **"Secret"-typed environment variables** — Postman's `secret` flag is a UI-mask only, the API returns the plaintext value to any read-capable key + +### Scan every workspace, collection, and environment visible to the API key + +```bash +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 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-environment 12345678-abcd-efgh-ijkl-mnopqrstuvwx +``` + +### Include mocks and monitors + +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 +``` + +### Self-hosted / enterprise endpoint + +```bash +KF_POSTMAN_TOKEN="PMAK-..." kingfisher scan --postman-all \ + --postman-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. + ## Environment Variables | Variable | Purpose | @@ -618,6 +673,7 @@ The token must be a Microsoft Graph API access token with `ChannelMessage.Read.A | `KF_CONFLUENCE_TOKEN` | Confluence API token | | `KF_SLACK_TOKEN` | Slack API token | | `KF_TEAMS_TOKEN` | Microsoft Graph API token for Teams message search | +| `KF_POSTMAN_TOKEN` / `POSTMAN_API_KEY` | Postman API key (sent as `X-Api-Key`) for workspace, collection, and environment scanning | | `KF_DOCKER_TOKEN` | Docker registry token (`user:pass` or bearer token). If unset, credentials from the Docker keychain are used | | `KF_AWS_KEY`, `KF_AWS_SECRET`, and `KF_AWS_SESSION_TOKEN` | AWS credentials for S3 bucket scanning. Session token is optional, for temporary credentials | diff --git a/docs/USAGE.md b/docs/USAGE.md index b4cd2cc..b4888e0 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -514,12 +514,11 @@ kingfisher scan ./my-project \ ## Scanning Platform-Specific Targets > **Deprecated** -> Legacy scan flags such as `--github-user`, `--gitlab-group`, -> `--bitbucket-workspace`, `--azure-organization`, `--huggingface-user`, +> Older documentation may refer to legacy provider flags such as +> `--github-user`, `--gitlab-group`, `--bitbucket-workspace`, > `--slack-query`, `--jira-url`, `--confluence-url`, `--s3-bucket`, -> `--gcs-bucket`, and `--docker-image` still work for now, but they trigger a -> warning and will be removed in a future release. Migrate to the -> `kingfisher scan ` subcommands below to future-proof your automations. +> `--gcs-bucket`, and `--docker-image`. Use the +> `kingfisher scan ` subcommands below instead. --- @@ -761,10 +760,10 @@ kingfisher scan gitlab --group my-group --gitlab-exclude my-group/**/legacy-* -- ### Scan Azure Repos organization or collection (requires `KF_AZURE_TOKEN` or `KF_AZURE_PAT`) ```bash -kingfisher scan azure --organization my-org +kingfisher scan azure --azure-organization my-org # Azure Repos Server example -KF_AZURE_PAT="pat" kingfisher scan azure --organization DefaultCollection --azure-base-url https://ado.internal.example/tfs/ +KF_AZURE_PAT="pat" kingfisher scan azure --azure-organization DefaultCollection --base-url https://ado.internal.example/tfs/ ``` ### Scan specific Azure Repos projects @@ -772,8 +771,8 @@ KF_AZURE_PAT="pat" kingfisher scan azure --organization DefaultCollection --azur Projects are specified as `ORGANIZATION/PROJECT`. Repeat the flag for multiple projects. ```bash -kingfisher scan azure --project my-org/payments \ - --project my-org/core-platform +kingfisher scan azure --azure-project my-org/payments \ + --azure-project my-org/core-platform ``` ### Skip specific Azure repositories during enumeration @@ -781,7 +780,7 @@ kingfisher scan azure --project my-org/payments \ Repeat `--azure-exclude` to ignore repositories when scanning organizations or projects. Use identifiers like `ORGANIZATION/PROJECT/REPOSITORY`. Repositories that share the same name as their project can be excluded with `ORGANIZATION/PROJECT`, and gitignore-style patterns such as `my-org/*/archive-*` are also supported. ```bash -kingfisher scan azure --organization my-org \ +kingfisher scan azure --azure-organization my-org \ --azure-exclude my-org/payments/legacy-service \ --azure-exclude my-org/**/archive-* ``` @@ -789,11 +788,11 @@ kingfisher scan azure --organization my-org \ ### List Azure repositories ```bash -kingfisher scan azure --organization my-org --list-only +kingfisher scan azure --azure-organization my-org --list-only # list repositories for specific projects -kingfisher scan azure --project my-org/app --project my-org/api --list-only +kingfisher scan azure --azure-project my-org/app --azure-project my-org/api --list-only # skip specific repositories while listing (supports glob patterns) -kingfisher scan azure --organization my-org --azure-exclude my-org/**/experimental-* --list-only +kingfisher scan azure --azure-organization my-org --azure-exclude my-org/**/experimental-* --list-only ``` --- @@ -805,7 +804,7 @@ kingfisher scan azure --organization my-org --azure-exclude my-org/**/experiment ```bash kingfisher scan gitea --organization my-org # self-hosted example -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --organization platform --gitea-api-url https://gitea.internal.example/api/v1/ +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --organization platform --api-url https://gitea.internal.example/api/v1/ ``` ### Scan Gitea user @@ -842,9 +841,9 @@ KF_GITEA_TOKEN="gtoken" KF_GITEA_USERNAME="org" \ ```bash kingfisher scan gitea --organization my-org --list-only # enumerate every organization visible to the authenticated user -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --all-gitea-organizations --list-only +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --all-organizations --list-only # self-hosted example -KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --user johndoe --gitea-api-url https://gitea.internal.example/api/v1/ --list-only +KF_GITEA_TOKEN="gtoken" kingfisher scan gitea --user johndoe --api-url https://gitea.internal.example/api/v1/ --list-only ``` --- @@ -917,7 +916,7 @@ Bitbucket no longer supports App Tokens as of September 9, 2025: https://support ### Self-hosted Bitbucket Server -Use `--bitbucket-api-url` to point Kingfisher at your server's REST endpoint, for example `https://bitbucket.example.com/rest/api/1.0/`. Provide credentials with `KF_BITBUCKET_USERNAME` plus either `KF_BITBUCKET_TOKEN` or `KF_BITBUCKET_PASSWORD`, and pass `--tls-mode=off` (or the legacy `--ignore-certs`) when connecting to HTTP or otherwise insecure instances. +Use `--api-url` to point Kingfisher at your server's REST endpoint, for example `https://bitbucket.example.com/rest/api/1.0/`. Provide credentials with `KF_BITBUCKET_USERNAME` plus either `KF_BITBUCKET_TOKEN` or `KF_BITBUCKET_PASSWORD`, and pass `--tls-mode=off` (or the legacy `--ignore-certs`) when connecting to HTTP or otherwise insecure instances. --- @@ -928,13 +927,13 @@ Hugging Face hosts git repositories for models, datasets, and Spaces. Kingfisher ### Scan Hugging Face user ```bash -kingfisher scan huggingface --user +kingfisher scan huggingface --huggingface-user ``` ### Scan Hugging Face organization ```bash -kingfisher scan huggingface --organization +kingfisher scan huggingface --huggingface-organization ``` ### Scan specific Hugging Face resources @@ -942,9 +941,9 @@ kingfisher scan huggingface --organization Scan individual repositories by ID (owner/name) or by passing the full HTTPS URL: ```bash -kingfisher scan huggingface --model -kingfisher scan huggingface --dataset https://huggingface.co/datasets// -kingfisher scan huggingface --space +kingfisher scan huggingface --huggingface-model +kingfisher scan huggingface --huggingface-dataset https://huggingface.co/datasets// +kingfisher scan huggingface --huggingface-space ``` Use `--huggingface-exclude` to omit results returned by user or organization enumeration. Prefix values with `model:`, `dataset:`, or `space:` when you only want to skip a specific resource type. @@ -952,7 +951,7 @@ Use `--huggingface-exclude` to omit results returned by user or organization enu ### List Hugging Face repositories ```bash -kingfisher scan huggingface --user --list-only +kingfisher scan huggingface --huggingface-user --list-only ``` ### Authenticate to Hugging Face @@ -1010,7 +1009,7 @@ KF_CONFLUENCE_USER="user@example.com" KF_CONFLUENCE_TOKEN="token" \ --max-results 500 ``` -Use the base URL of your Confluence site for `--confluence-url`. Kingfisher automatically adds `/rest/api` to the end, so `https://example.com/wiki` and `https://example.com` both work depending on your server configuration. +Use the base URL of your Confluence site for `--url`. Kingfisher automatically adds `/rest/api` to the end, so `https://example.com/wiki` and `https://example.com` both work depending on your server configuration. Generate a personal access token and set it in the `KF_CONFLUENCE_TOKEN` environment variable. By default, Kingfisher sends the token as a bearer token in the `Authorization` header. diff --git a/src/access_map.rs b/src/access_map.rs index 9fa4bf1..83d0179 100644 --- a/src/access_map.rs +++ b/src/access_map.rs @@ -1,8 +1,10 @@ +use std::io::Write; + use anyhow::Result; use schemars::JsonSchema; use serde::Serialize; -use crate::cli::commands::access_map::{AccessMapArgs, AccessMapProvider}; +use crate::cli::commands::access_map::{AccessMapArgs, AccessMapOutputFormat, AccessMapProvider}; mod airtable; mod algolia; @@ -112,15 +114,16 @@ pub async fn run(args: AccessMapArgs) -> Result<()> { AccessMapProvider::Asana => asana::map_access(&args).await?, }; - let json = serde_json::to_string_pretty(&result)?; - if let Some(path) = args.json_out { - std::fs::write(path, json)?; - } else { - println!("{json}"); - } - - if let Some(path) = args.html_out { - report::generate_html_report_multi(&[result], &path)?; + let mut writer = args.output_args.get_writer()?; + match args.output_args.format { + AccessMapOutputFormat::Json => { + serde_json::to_writer_pretty(&mut writer, &result)?; + writeln!(writer)?; + } + AccessMapOutputFormat::Html => { + let html = report::render_html_report_multi(&[result])?; + writer.write_all(html.as_bytes())?; + } } Ok(()) diff --git a/src/access_map/report.rs b/src/access_map/report.rs index 9ae2c1e..21f5383 100644 --- a/src/access_map/report.rs +++ b/src/access_map/report.rs @@ -10,13 +10,18 @@ use super::AccessMapResult; /// Generate a standalone HTML report with a simple, collapsible tree view (no D3 dependency). pub fn generate_html_report_multi(results: &[AccessMapResult], path: &Path) -> Result<()> { - let json = serde_json::to_string(results)?; - let compressed = gzip_base64(&json)?; - let html = build_html(&json, &compressed); + let html = render_html_report_multi(results)?; std::fs::write(path, html)?; Ok(()) } +/// Render a standalone HTML report for a collection of access-map results. +pub fn render_html_report_multi(results: &[AccessMapResult]) -> Result { + let json = serde_json::to_string(results)?; + let compressed = gzip_base64(&json)?; + Ok(build_html(&json, &compressed)) +} + fn gzip_base64(json_str: &str) -> Result { let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); encoder.write_all(json_str.as_bytes())?; diff --git a/src/cli/commands/access_map.rs b/src/cli/commands/access_map.rs index 3ea7e38..996bfdb 100644 --- a/src/cli/commands/access_map.rs +++ b/src/cli/commands/access_map.rs @@ -1,6 +1,9 @@ use std::path::PathBuf; -use clap::{Args, ValueEnum}; +use clap::{Args, ValueEnum, ValueHint}; +use strum::Display; + +use crate::util::get_writer_for_file_or_stdout; /// Inspect a cloud credential and derive the effective identity and blast radius. #[derive(Args, Debug)] @@ -13,13 +16,37 @@ pub struct AccessMapArgs { #[clap(value_parser, value_name = "CREDENTIAL", required = false)] pub credential_path: Option, - /// Optional path to write an interactive D3.js HTML report - #[clap(long, value_name = "PATH")] - pub html_out: Option, + #[command(flatten)] + pub output_args: AccessMapOutputArgs, +} - /// Optional path to write JSON output (otherwise JSON goes to stdout) - #[clap(long, value_name = "PATH")] - pub json_out: Option, +#[derive(Args, Debug, Clone)] +#[command(next_help_heading = "Output Options")] +pub struct AccessMapOutputArgs { + /// Write output to the specified path (stdout if not given) + #[arg(long, short = 'o', value_hint = ValueHint::FilePath)] + pub output: Option, + + /// Output format + #[arg(long, short = 'f', default_value = "json")] + pub format: AccessMapOutputFormat, +} + +impl AccessMapOutputArgs { + /// Return a writer for the specified output destination + pub fn get_writer(&self) -> std::io::Result> { + get_writer_for_file_or_stdout(self.output.as_ref()) + } +} + +#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +#[strum(serialize_all = "kebab-case")] +pub enum AccessMapOutputFormat { + /// Pretty-printed JSON + Json, + + /// Standalone HTML access-map report + Html, } /// Supported cloud providers for identity mapping. diff --git a/src/cli/commands/inputs.rs b/src/cli/commands/inputs.rs index 95d3cbd..9d59258 100644 --- a/src/cli/commands/inputs.rs +++ b/src/cli/commands/inputs.rs @@ -22,6 +22,7 @@ const DEFAULT_BITBUCKET_API_URL: &str = "https://api.bitbucket.org/2.0/"; const DEFAULT_AZURE_BASE_URL: &str = "https://dev.azure.com/"; const DEFAULT_SLACK_API_URL: &str = "https://slack.com/api/"; const DEFAULT_TEAMS_API_URL: &str = "https://graph.microsoft.com/"; +const DEFAULT_POSTMAN_API_URL: &str = "https://api.getpostman.com/"; // ----------------------------------------------------------------------------- // Inputs @@ -295,7 +296,40 @@ pub struct InputSpecifierArgs { #[arg(long, default_value = "https://graph.microsoft.com/", value_hint = ValueHint::Url, hide = true)] pub teams_api_url: Url, - /// Maximum number of Slack, Teams, Jira, or Confluence results to fetch + /// Scan a Postman workspace by ID or web URL (repeatable) + #[arg(long = "postman-workspace", value_name = "ID_OR_URL", hide = true)] + pub postman_workspaces: Vec, + + /// Scan a single Postman collection by UID or web URL (repeatable) + #[arg(long = "postman-collection", value_name = "UID_OR_URL", hide = true)] + pub postman_collections: Vec, + + /// Scan a single Postman environment by UID (repeatable) + #[arg(long = "postman-environment", value_name = "UID", hide = true)] + pub postman_environments: Vec, + + /// Scan every workspace, collection, and environment visible to the API key + #[arg( + long = "postman-all", + hide = true, + conflicts_with_all = ["postman_workspaces", "postman_collections", "postman_environments"], + )] + pub postman_all: bool, + + /// Include Postman mocks and monitors when scanning a workspace (off by default) + #[arg(long = "postman-include-mocks-monitors", hide = true)] + pub postman_include_mocks_monitors: bool, + + /// Use the specified base URL for the Postman API (e.g. self-hosted) + #[arg( + long = "postman-api-url", + default_value = DEFAULT_POSTMAN_API_URL, + value_hint = ValueHint::Url, + hide = true, + )] + pub postman_api_url: Url, + + /// Maximum number of Slack, Teams, Jira, Confluence, or Postman results to fetch #[arg(long, default_value_t = 100, hide = true)] pub max_results: usize, diff --git a/src/cli/commands/scan.rs b/src/cli/commands/scan.rs index d8bc476..b7b486c 100644 --- a/src/cli/commands/scan.rs +++ b/src/cli/commands/scan.rs @@ -508,6 +508,26 @@ impl ScanCommandArgs { scan_args.input_specifier_args.max_results = args.max_results; None } + ScanInputCommand::Postman(args) => { + if !args.all + && args.workspaces.is_empty() + && args.collections.is_empty() + && args.environments.is_empty() + { + bail!( + "Specify --workspace, --collection, --environment, or --all when using the postman subcommand" + ); + } + scan_args.input_specifier_args.postman_workspaces = args.workspaces; + scan_args.input_specifier_args.postman_collections = args.collections; + scan_args.input_specifier_args.postman_environments = args.environments; + scan_args.input_specifier_args.postman_all = args.all; + scan_args.input_specifier_args.postman_include_mocks_monitors = + args.include_mocks_monitors; + scan_args.input_specifier_args.postman_api_url = args.api_url; + scan_args.input_specifier_args.max_results = args.max_results; + None + } ScanInputCommand::S3(args) => { scan_args.input_specifier_args.s3_bucket = Some(args.bucket); scan_args.input_specifier_args.s3_prefix = args.prefix; @@ -649,6 +669,9 @@ pub enum ScanInputCommand { /// Scan Confluence content using CQL Confluence(ConfluenceScanArgs), + /// Scan Postman workspaces, collections, and environments + Postman(PostmanScanArgs), + /// Scan an S3 bucket S3(S3ScanArgs), @@ -869,6 +892,46 @@ pub struct ConfluenceScanArgs { pub max_results: usize, } +#[derive(Args, Debug, Clone)] +pub struct PostmanScanArgs { + /// Scan a Postman workspace by ID or web URL (repeatable) + #[arg(long = "workspace", alias = "postman-workspace", value_name = "ID_OR_URL")] + pub workspaces: Vec, + + /// Scan a single Postman collection by UID or web URL (repeatable) + #[arg(long = "collection", alias = "postman-collection", value_name = "UID_OR_URL")] + pub collections: Vec, + + /// Scan a single Postman environment by UID (repeatable) + #[arg(long = "environment", alias = "postman-environment", value_name = "UID")] + pub environments: Vec, + + /// Scan every workspace, collection, and environment visible to the API key + #[arg( + long = "all", + alias = "postman-all", + conflicts_with_all = ["workspaces", "collections", "environments"], + )] + pub all: bool, + + /// Include Postman mocks and monitors when scanning a workspace (off by default) + #[arg(long = "include-mocks-monitors", alias = "postman-include-mocks-monitors")] + pub include_mocks_monitors: bool, + + /// Override the Postman API base URL + #[arg( + long = "api-url", + alias = "postman-api-url", + default_value = "https://api.getpostman.com/", + value_hint = ValueHint::Url, + )] + pub api_url: Url, + + /// Maximum number of resources to fetch + #[arg(long = "max-results", default_value_t = 100)] + pub max_results: usize, +} + #[derive(Args, Debug, Clone)] pub struct S3ScanArgs { /// S3 bucket to scan diff --git a/src/direct_validate.rs b/src/direct_validate.rs index 489189c..508edec 100644 --- a/src/direct_validate.rs +++ b/src/direct_validate.rs @@ -990,6 +990,12 @@ pub(crate) fn create_minimal_scan_args() -> crate::cli::commands::scan::ScanArgs slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), docker_image: Vec::new(), git_clone: GitCloneMode::Bare, git_history: GitHistoryMode::Full, diff --git a/src/findings_store.rs b/src/findings_store.rs index e315991..7a4b4a2 100644 --- a/src/findings_store.rs +++ b/src/findings_store.rs @@ -108,6 +108,7 @@ pub struct FindingsStore { slack_links: FxHashMap, teams_links: FxHashMap, confluence_links: FxHashMap, + postman_links: FxHashMap, s3_buckets: FxHashMap, repo_links: FxHashMap, access_map_results: Vec, @@ -129,6 +130,7 @@ impl FindingsStore { slack_links: FxHashMap::default(), teams_links: FxHashMap::default(), confluence_links: FxHashMap::default(), + postman_links: FxHashMap::default(), s3_buckets: FxHashMap::default(), repo_links: FxHashMap::default(), access_map_results: Vec::new(), @@ -396,6 +398,14 @@ impl FindingsStore { &self.confluence_links } + pub fn register_postman_resource(&mut self, path: PathBuf, link: String) { + self.postman_links.insert(path, link); + } + + pub fn postman_links(&self) -> &FxHashMap { + &self.postman_links + } + pub fn register_repo_link(&mut self, path: PathBuf, link: String) { self.repo_links.insert(path, link); } @@ -437,6 +447,10 @@ impl FindingsStore { self.confluence_links.entry(dir.clone()).or_insert_with(|| link.clone()); } + for (dir, link) in other.postman_links() { + self.postman_links.entry(dir.clone()).or_insert_with(|| link.clone()); + } + let batch: Vec<_> = other .get_matches() .iter() @@ -511,14 +525,19 @@ mod tests { #[test] fn dedup_filter_remains_monotonic_across_growth() { + // capacity=2 triggers growth after two insertions. With a 2-item Bloom + // filter (~29 bits, 10 hash functions), the third item has ~2.5% FP + // probability, so we only assert "new" for items 1-2 (inserted into + // an empty or near-empty filter where FP is negligible) and just + // trigger growth for item 3 without asserting its novelty. let mut filter = DedupBloomSet::with_capacity(2); assert!(!filter.contains_or_insert(11)); assert!(!filter.contains_or_insert(22)); - assert!(!filter.contains_or_insert(33)); + let _ = filter.contains_or_insert(33); // triggers growth; FP-rate too high to assert - assert!(filter.contains_or_insert(11)); - assert!(filter.contains_or_insert(22)); - assert!(filter.contains_or_insert(33)); + assert!(filter.contains_or_insert(11), "item 11 must be found after growth"); + assert!(filter.contains_or_insert(22), "item 22 must be found after growth"); + assert!(filter.contains_or_insert(33), "item 33 must be found after growth"); } } diff --git a/src/lib.rs b/src/lib.rs index 33d6490..369820b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,7 @@ pub mod location; pub mod matcher; pub mod origin; pub mod parser; +pub mod postman; pub mod provider_endpoints; pub mod pyc; pub mod reporter; diff --git a/src/main.rs b/src/main.rs index b626e4a..759ef09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -585,6 +585,12 @@ fn create_default_scan_args() -> cli::commands::scan::ScanArgs { teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), // Docker image scanning docker_image: Vec::new(), diff --git a/src/postman.rs b/src/postman.rs new file mode 100644 index 0000000..d1bec8c --- /dev/null +++ b/src/postman.rs @@ -0,0 +1,395 @@ +use anyhow::{Context, Result, bail}; +use reqwest::{Client, StatusCode}; +use serde::Deserialize; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use tokio::time::sleep; +use tracing::{debug, warn}; +use url::Url; + +#[derive(Debug, Clone, Default)] +pub struct PostmanSelectors { + pub workspaces: Vec, + pub collections: Vec, + pub environments: Vec, + pub all: bool, + pub include_mocks_monitors: bool, +} + +impl PostmanSelectors { + pub fn is_empty(&self) -> bool { + !self.all + && self.workspaces.is_empty() + && self.collections.is_empty() + && self.environments.is_empty() + } +} + +#[derive(Debug, Deserialize)] +struct WorkspacesEnvelope { + #[serde(default)] + workspaces: Vec, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceSummary { + id: String, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceDetailEnvelope { + workspace: WorkspaceDetail, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceDetail { + id: String, + #[serde(default)] + collections: Vec, + #[serde(default)] + environments: Vec, + #[serde(default)] + mocks: Vec, + #[serde(default)] + monitors: Vec, +} + +#[derive(Debug, Deserialize)] +struct RefItem { + #[serde(default)] + id: Option, + #[serde(default)] + uid: Option, +} + +impl RefItem { + fn pick(&self) -> Option<&str> { + self.uid.as_deref().or(self.id.as_deref()) + } +} + +const MAX_RETRIES: usize = 5; + +fn token_from_env() -> Result { + if let Ok(t) = std::env::var("KF_POSTMAN_TOKEN") + && !t.is_empty() + { + return Ok(t); + } + if let Ok(t) = std::env::var("POSTMAN_API_KEY") + && !t.is_empty() + { + return Ok(t); + } + bail!("KF_POSTMAN_TOKEN (or POSTMAN_API_KEY) environment variable must be set"); +} + +/// Best-effort UID extraction. Accepts: +/// - bare UID strings (returned unchanged) +/// - Postman web URLs: take the last URL path segment +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(); + } + input.to_string() +} + +async fn get_with_retries( + client: &Client, + url: Url, + token: &str, +) -> Result> { + let mut attempt = 0; + loop { + attempt += 1; + let resp = client + .get(url.clone()) + .header("X-Api-Key", token) + .header("Accept", "application/json") + .send() + .await + .with_context(|| format!("Failed to send Postman request to {}", url))?; + + let status = resp.status(); + if status == StatusCode::TOO_MANY_REQUESTS && attempt <= MAX_RETRIES { + let retry_after = resp + .headers() + .get("X-RateLimit-RetryAfter") + .or_else(|| resp.headers().get("Retry-After")) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()) + .unwrap_or(1); + warn!( + "Postman API rate-limited at {} (attempt {}). Sleeping {}s", + url, attempt, retry_after + ); + sleep(Duration::from_secs(retry_after)).await; + continue; + } + if status == StatusCode::NOT_FOUND || status == StatusCode::FORBIDDEN { + debug!("Postman API returned {} for {} (skipping)", status, url); + return Ok(None); + } + if !status.is_success() { + let body = resp.text().await.unwrap_or_default(); + bail!("Postman API request to {} failed with status {}: {}", url, status, body); + } + let value: serde_json::Value = + resp.json().await.with_context(|| format!("Failed to parse JSON from {}", url))?; + return Ok(Some(value)); + } +} + +fn web_url_for_collection(uid: &str) -> String { + format!("https://go.postman.co/collection/{}", uid) +} + +fn web_url_for_environment(uid: &str) -> String { + format!("https://go.postman.co/environments/{}", uid) +} + +fn web_url_for_workspace(id: &str) -> String { + format!("https://go.postman.co/workspace/{}", id) +} + +fn web_url_for_mock(uid: &str) -> String { + format!("https://go.postman.co/mock/{}", uid) +} + +fn web_url_for_monitor(uid: &str) -> String { + format!("https://go.postman.co/monitor/{}", uid) +} + +async fn fetch_workspace_ids(client: &Client, api_url: &Url, token: &str) -> Result> { + let url = api_url.join("workspaces").context("Failed to build workspaces URL")?; + let Some(value) = get_with_retries(client, url, token).await? else { + return Ok(Vec::new()); + }; + let envelope: WorkspacesEnvelope = + serde_json::from_value(value).context("Failed to parse Postman workspaces response")?; + Ok(envelope.workspaces.into_iter().map(|w| w.id).collect()) +} + +async fn fetch_workspace_detail( + client: &Client, + api_url: &Url, + token: &str, + id: &str, +) -> Result> { + let url = api_url + .join(&format!("workspaces/{}", id)) + .with_context(|| format!("Failed to build workspace URL for {}", id))?; + let Some(value) = get_with_retries(client, url, token).await? else { + return Ok(None); + }; + let envelope: WorkspaceDetailEnvelope = serde_json::from_value(value.clone()) + .with_context(|| format!("Failed to parse workspace {} response", id))?; + Ok(Some((value, envelope.workspace))) +} + +async fn fetch_resource( + client: &Client, + api_url: &Url, + token: &str, + path: &str, +) -> Result> { + let url = api_url.join(path).with_context(|| format!("Failed to build URL for {}", path))?; + get_with_retries(client, url, token).await +} + +async fn write_json(dir: &Path, name: &str, value: &serde_json::Value) -> Result { + tokio::fs::create_dir_all(dir).await?; + let path = dir.join(name); + tokio::fs::write(&path, serde_json::to_vec_pretty(value)?).await?; + Ok(path) +} + +pub async fn download_postman_to_dir( + api_url: Url, + selectors: PostmanSelectors, + max_results: usize, + ignore_certs: bool, + output_dir: &Path, +) -> Result> { + let token = token_from_env()?; + let client = Client::builder() + .danger_accept_invalid_certs(ignore_certs) + .build() + .context("Failed to build HTTP client")?; + + std::fs::create_dir_all(output_dir)?; + + let mut paths: Vec<(PathBuf, String)> = Vec::new(); + + // Track UIDs we've already fetched to avoid duplicate API calls when + // the same collection/environment is referenced from multiple workspaces. + let mut seen_collections = std::collections::HashSet::new(); + let mut seen_environments = std::collections::HashSet::new(); + let mut seen_mocks = std::collections::HashSet::new(); + let mut seen_monitors = std::collections::HashSet::new(); + let mut seen_workspaces = std::collections::HashSet::new(); + + // Resolve workspace selectors (explicit list and/or --all) + let mut workspace_ids: Vec = + selectors.workspaces.iter().map(|s| resolve_uid(s)).collect(); + if selectors.all { + let listed = fetch_workspace_ids(&client, &api_url, &token).await?; + for id in listed { + if !workspace_ids.contains(&id) { + workspace_ids.push(id); + } + } + } + + // Walk workspaces -> collect collection/environment/mock/monitor UIDs + let mut collection_uids: Vec = + selectors.collections.iter().map(|s| resolve_uid(s)).collect(); + let mut environment_uids: Vec = + selectors.environments.iter().map(|s| resolve_uid(s)).collect(); + let mut mock_uids: Vec = Vec::new(); + let mut monitor_uids: Vec = Vec::new(); + + for ws_id in workspace_ids { + if !seen_workspaces.insert(ws_id.clone()) { + continue; + } + let Some((raw, detail)) = fetch_workspace_detail(&client, &api_url, &token, &ws_id).await? + else { + continue; + }; + let path = write_json(output_dir, &format!("workspace_{}.json", detail.id), &raw).await?; + paths.push((path, web_url_for_workspace(&detail.id))); + + for c in detail.collections { + if let Some(uid) = c.pick() { + let uid = uid.to_string(); + if !collection_uids.contains(&uid) { + collection_uids.push(uid); + } + } + } + for e in detail.environments { + if let Some(uid) = e.pick() { + let uid = uid.to_string(); + if !environment_uids.contains(&uid) { + environment_uids.push(uid); + } + } + } + if selectors.include_mocks_monitors { + for m in detail.mocks { + if let Some(uid) = m.pick() { + mock_uids.push(uid.to_string()); + } + } + for m in detail.monitors { + if let Some(uid) = m.pick() { + monitor_uids.push(uid.to_string()); + } + } + } + } + + let limit_hit = |paths: &Vec<(PathBuf, String)>| max_results > 0 && paths.len() >= max_results; + + for uid in collection_uids { + if limit_hit(&paths) { + break; + } + if !seen_collections.insert(uid.clone()) { + continue; + } + let Some(value) = + fetch_resource(&client, &api_url, &token, &format!("collections/{}", uid)).await? + else { + continue; + }; + let path = write_json(output_dir, &format!("collection_{}.json", uid), &value).await?; + paths.push((path, web_url_for_collection(&uid))); + } + + for uid in environment_uids { + if limit_hit(&paths) { + break; + } + if !seen_environments.insert(uid.clone()) { + continue; + } + let Some(value) = + fetch_resource(&client, &api_url, &token, &format!("environments/{}", uid)).await? + else { + continue; + }; + let path = write_json(output_dir, &format!("environment_{}.json", uid), &value).await?; + paths.push((path, web_url_for_environment(&uid))); + } + + for uid in mock_uids { + if limit_hit(&paths) { + break; + } + if !seen_mocks.insert(uid.clone()) { + continue; + } + let Some(value) = + fetch_resource(&client, &api_url, &token, &format!("mocks/{}", uid)).await? + else { + continue; + }; + let path = write_json(output_dir, &format!("mock_{}.json", uid), &value).await?; + paths.push((path, web_url_for_mock(&uid))); + } + + for uid in monitor_uids { + if limit_hit(&paths) { + break; + } + if !seen_monitors.insert(uid.clone()) { + continue; + } + let Some(value) = + fetch_resource(&client, &api_url, &token, &format!("monitors/{}", uid)).await? + else { + continue; + }; + let path = write_json(output_dir, &format!("monitor_{}.json", uid), &value).await?; + paths.push((path, web_url_for_monitor(&uid))); + } + + Ok(paths) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_uid_passes_through_bare_ids() { + assert_eq!(resolve_uid("12345-abc"), "12345-abc"); + assert_eq!( + resolve_uid("11111111-2222-3333-4444-555555555555"), + "11111111-2222-3333-4444-555555555555" + ); + } + + #[test] + fn resolve_uid_extracts_last_segment_from_url() { + 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"); + } + + #[test] + fn selectors_is_empty_by_default() { + assert!(PostmanSelectors::default().is_empty()); + let sel = PostmanSelectors { workspaces: vec!["a".into()], ..PostmanSelectors::default() }; + assert!(!sel.is_empty()); + } +} diff --git a/src/reporter.rs b/src/reporter.rs index b76c0ed..e4b0623 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -706,6 +706,11 @@ impl DetailsReporter { ds.teams_links().get(path).cloned() } + fn postman_resource_url(&self, path: &std::path::Path) -> Option { + let ds = self.datastore.lock().ok()?; + ds.postman_links().get(path).cloned() + } + fn repo_artifact_url(&self, path: &std::path::Path) -> Option { let ds = self.datastore.lock().ok()?; ds.repo_links().get(path).cloned() @@ -1064,6 +1069,7 @@ impl DetailsReporter { .or_else(|| self.confluence_page_url(&e.path).and_then(Self::non_empty_string)) .or_else(|| self.slack_message_url(&e.path).and_then(Self::non_empty_string)) .or_else(|| self.teams_message_url(&e.path).and_then(Self::non_empty_string)) + .or_else(|| self.postman_resource_url(&e.path).and_then(Self::non_empty_string)) .or_else(|| self.s3_display_path(&e.path).and_then(Self::non_empty_string)) .or_else(|| self.docker_display_path(&e.path).and_then(Self::non_empty_string)) .or_else(|| Self::non_empty_string(e.path.display().to_string())), @@ -1726,6 +1732,12 @@ mod tests { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), max_results: 100, s3_bucket: None, s3_prefix: None, diff --git a/src/reporter/json_format.rs b/src/reporter/json_format.rs index c5b3500..71bb599 100644 --- a/src/reporter/json_format.rs +++ b/src/reporter/json_format.rs @@ -151,6 +151,12 @@ mod tests { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), // s3 s3_bucket: None, s3_prefix: None, diff --git a/src/scanner/repos.rs b/src/scanner/repos.rs index d58e050..f38f656 100644 --- a/src/scanner/repos.rs +++ b/src/scanner/repos.rs @@ -25,6 +25,7 @@ use crate::{ gitea, github, gitlab, huggingface, jira, matcher::{Match, Matcher, MatcherStats}, origin::{Origin, OriginSet}, + postman, rules_database::RulesDatabase, s3, scanner::processing::BlobProcessor, @@ -741,6 +742,45 @@ pub async fn fetch_slack_messages( Ok(vec![output_dir]) } +pub async fn fetch_postman_resources( + args: &scan::ScanArgs, + global_args: &global::GlobalArgs, + datastore: &Arc>, +) -> Result> { + let selectors = postman::PostmanSelectors { + workspaces: args.input_specifier_args.postman_workspaces.clone(), + collections: args.input_specifier_args.postman_collections.clone(), + environments: args.input_specifier_args.postman_environments.clone(), + all: args.input_specifier_args.postman_all, + include_mocks_monitors: args.input_specifier_args.postman_include_mocks_monitors, + }; + if selectors.is_empty() { + return Ok(Vec::new()); + } + let api_url = args.input_specifier_args.postman_api_url.clone(); + let max_results = args.input_specifier_args.max_results; + let output_root = { + let ds = datastore.lock().unwrap(); + ds.clone_root() + }; + let output_dir = output_root.join("postman"); + let paths = postman::download_postman_to_dir( + api_url, + selectors, + max_results, + global_args.ignore_certs, + &output_dir, + ) + .await?; + { + let mut ds = datastore.lock().unwrap(); + for (path, link) in &paths { + ds.register_postman_resource(path.clone(), link.clone()); + } + } + Ok(vec![output_dir]) +} + pub async fn fetch_teams_messages( args: &scan::ScanArgs, global_args: &global::GlobalArgs, diff --git a/src/scanner/runner.rs b/src/scanner/runner.rs index bbc25c5..b9d744f 100644 --- a/src/scanner/runner.rs +++ b/src/scanner/runner.rs @@ -36,8 +36,8 @@ use crate::{ enumerate_huggingface_repos, repos::{ enumerate_gitea_repos, enumerate_gitlab_repos, fetch_confluence_pages, - fetch_gcs_objects, fetch_git_host_artifacts, fetch_jira_issues, fetch_s3_objects, - fetch_slack_messages, fetch_teams_messages, + fetch_gcs_objects, fetch_git_host_artifacts, fetch_jira_issues, + fetch_postman_resources, fetch_s3_objects, fetch_slack_messages, fetch_teams_messages, }, run_secret_validation, save_docker_images, summary::{compute_scan_totals, print_scan_summary}, @@ -380,6 +380,10 @@ async fn fetch_all_artifacts( let teams_dirs = fetch_teams_messages(args, global_args, datastore).await?; input_roots.extend(teams_dirs); + // Fetch Postman resources if requested + let postman_dirs = fetch_postman_resources(args, global_args, datastore).await?; + input_roots.extend(postman_dirs); + // Save Docker images if specified if !args.input_specifier_args.docker_image.is_empty() { let clone_root = { diff --git a/tests/cli_access_map_output.rs b/tests/cli_access_map_output.rs new file mode 100644 index 0000000..79f54ff --- /dev/null +++ b/tests/cli_access_map_output.rs @@ -0,0 +1,56 @@ +use clap::Parser; + +use kingfisher::cli::{ + commands::access_map::AccessMapOutputFormat, + global::{Command, CommandLineArgs}, +}; + +#[test] +fn access_map_accepts_format_and_output_flags() -> anyhow::Result<()> { + let args = CommandLineArgs::try_parse_from([ + "kingfisher", + "access-map", + "gitlab", + "./gitlab.token", + "--format", + "json", + "--output", + "gitlab.access-map.json", + "--no-update-check", + ])?; + + let command = match args.command { + Command::AccessMap(args) => args, + other => panic!("unexpected command parsed: {:?}", other), + }; + + assert_eq!(command.output_args.format, AccessMapOutputFormat::Json); + assert_eq!( + command.output_args.output.as_deref(), + Some(std::path::Path::new("gitlab.access-map.json")) + ); + + Ok(()) +} + +#[test] +fn access_map_rejects_legacy_output_flags() { + for legacy_flag in ["--json-out", "--html-out"] { + let err = CommandLineArgs::try_parse_from([ + "kingfisher", + "access-map", + "gitlab", + "./gitlab.token", + legacy_flag, + "out.json", + "--no-update-check", + ]) + .expect_err("legacy access-map output flags should be rejected"); + + let rendered = err.to_string(); + assert!( + rendered.contains(legacy_flag), + "expected error to mention {legacy_flag}: {rendered}" + ); + } +} diff --git a/tests/int_allowlist.rs b/tests/int_allowlist.rs index db74ecc..dd40e40 100644 --- a/tests/int_allowlist.rs +++ b/tests/int_allowlist.rs @@ -128,6 +128,12 @@ fn run_skiplist(skip_regex: Vec, skip_skipword: Vec) -> Result Result<()> { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), s3_bucket: None, s3_prefix: None, role_arn: None, diff --git a/tests/int_dedup.rs b/tests/int_dedup.rs index 76fa724..914c17b 100644 --- a/tests/int_dedup.rs +++ b/tests/int_dedup.rs @@ -131,6 +131,12 @@ rules: slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), // s3 s3_bucket: None, s3_prefix: None, diff --git a/tests/int_github.rs b/tests/int_github.rs index 4954278..14ed62e 100644 --- a/tests/int_github.rs +++ b/tests/int_github.rs @@ -118,6 +118,12 @@ fn test_github_remote_scan() -> Result<()> { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), // s3 s3_bucket: None, s3_prefix: None, diff --git a/tests/int_gitlab.rs b/tests/int_gitlab.rs index ce9fd52..7b54d46 100644 --- a/tests/int_gitlab.rs +++ b/tests/int_gitlab.rs @@ -117,6 +117,12 @@ fn test_gitlab_remote_scan() -> Result<()> { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), // s3 s3_bucket: None, s3_prefix: None, @@ -297,6 +303,12 @@ fn test_gitlab_remote_scan_no_history() -> Result<()> { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), s3_bucket: None, s3_prefix: None, role_arn: None, diff --git a/tests/int_postman.rs b/tests/int_postman.rs new file mode 100644 index 0000000..4c23658 --- /dev/null +++ b/tests/int_postman.rs @@ -0,0 +1,299 @@ +use std::sync::{Arc, Mutex}; + +use anyhow::Result; +use kingfisher::{ + cli::{ + GlobalArgs, + commands::{ + azure::AzureRepoType, + bitbucket::{BitbucketAuthArgs, BitbucketRepoType}, + gitea::GiteaRepoType, + github::{GitCloneMode, GitHistoryMode, GitHubRepoType}, + gitlab::GitLabRepoType, + inputs::{ContentFilteringArgs, InputSpecifierArgs}, + output::{OutputArgs, ReportOutputFormat}, + rules::RuleSpecifierArgs, + scan::{ConfidenceLevel, ScanArgs}, + }, + global::{Mode, TlsMode}, + }, + findings_store::FindingsStore, + rule_loader::RuleLoader, + rules_database::RulesDatabase, + scanner::run_async_scan, + update::UpdateStatus, +}; +use tempfile::TempDir; +use url::Url; +use wiremock::{ + Mock, MockServer, ResponseTemplate, + matchers::{method, path}, +}; + +#[tokio::test] +async fn test_scan_postman_all() -> Result<()> { + use std::env; + + let server = MockServer::start().await; + + // Workspace listing + let workspaces_response = serde_json::json!({ + "workspaces": [ + { "id": "ws-uid-1", "name": "demo", "type": "team", "visibility": "private" } + ] + }); + Mock::given(method("GET")) + .and(path("/workspaces")) + .respond_with(ResponseTemplate::new(200).set_body_json(workspaces_response)) + .mount(&server) + .await; + + // Workspace detail (collections + environments embedded) + let workspace_detail = serde_json::json!({ + "workspace": { + "id": "ws-uid-1", + "name": "demo", + "type": "team", + "collections": [ + { "id": "col-uid-1", "uid": "col-uid-1", "name": "demo collection" } + ], + "environments": [ + { "id": "env-uid-1", "uid": "env-uid-1", "name": "prod" } + ], + "mocks": [], + "monitors": [] + } + }); + Mock::given(method("GET")) + .and(path("/workspaces/ws-uid-1")) + .respond_with(ResponseTemplate::new(200).set_body_json(workspace_detail)) + .mount(&server) + .await; + + // Collection detail — plant a GitHub PAT in a request bearer token + let collection_response = serde_json::json!({ + "collection": { + "info": { "name": "demo collection" }, + "item": [{ + "name": "auth call", + "request": { + "method": "GET", + "header": [], + "url": { "raw": "https://api.example.com/v1/me" }, + "auth": { + "type": "bearer", + "bearer": [{ + "key": "token", + "value": "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", + "type": "string" + }] + } + } + }] + } + }); + Mock::given(method("GET")) + .and(path("/collections/col-uid-1")) + .respond_with(ResponseTemplate::new(200).set_body_json(collection_response)) + .mount(&server) + .await; + + // Environment detail — plant a second GitHub PAT in a "secret"-typed variable + // (this exercises the headline finding: the API returns plaintext for "secret" + // env vars, so Kingfisher can detect what the UI would otherwise mask). + let environment_response = serde_json::json!({ + "environment": { + "id": "env-uid-1", + "name": "prod", + "values": [ + { + "key": "API_TOKEN", + "value": "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", + "type": "secret", + "enabled": true + } + ] + } + }); + Mock::given(method("GET")) + .and(path("/environments/env-uid-1")) + .respond_with(ResponseTemplate::new(200).set_body_json(environment_response)) + .mount(&server) + .await; + + // TODO: Audit that the environment access only happens in single-threaded code. + unsafe { env::set_var("KF_POSTMAN_TOKEN", "test-key") }; + + let temp_dir = TempDir::new()?; + let clone_dir = temp_dir.path().to_path_buf(); + + let scan_args = ScanArgs { + num_jobs: 2, + rules: RuleSpecifierArgs { + rules_path: Vec::new(), + rule: vec!["all".into()], + load_builtins: true, + }, + input_specifier_args: InputSpecifierArgs { + path_inputs: Vec::new(), + git_url: Vec::new(), + git_clone_dir: None, + keep_clones: false, + repo_clone_limit: None, + include_contributors: false, + github_user: Vec::new(), + github_organization: Vec::new(), + github_exclude: Vec::new(), + all_github_organizations: false, + github_api_url: Url::parse("https://api.github.com/").unwrap(), + github_repo_type: GitHubRepoType::Source, + gitlab_user: Vec::new(), + gitlab_group: Vec::new(), + gitlab_exclude: Vec::new(), + all_gitlab_groups: false, + gitlab_api_url: Url::parse("https://gitlab.com/").unwrap(), + gitlab_repo_type: GitLabRepoType::Owner, + gitlab_include_subgroups: false, + huggingface_user: Vec::new(), + huggingface_organization: Vec::new(), + huggingface_model: Vec::new(), + huggingface_dataset: Vec::new(), + huggingface_space: Vec::new(), + huggingface_exclude: Vec::new(), + gitea_user: Vec::new(), + gitea_organization: Vec::new(), + gitea_exclude: Vec::new(), + all_gitea_organizations: false, + gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(), + gitea_repo_type: GiteaRepoType::Source, + bitbucket_user: Vec::new(), + bitbucket_workspace: Vec::new(), + bitbucket_project: Vec::new(), + bitbucket_exclude: Vec::new(), + all_bitbucket_workspaces: false, + bitbucket_api_url: Url::parse("https://api.bitbucket.org/2.0/").unwrap(), + bitbucket_repo_type: BitbucketRepoType::Source, + bitbucket_auth: BitbucketAuthArgs::default(), + azure_organization: Vec::new(), + azure_project: Vec::new(), + azure_exclude: Vec::new(), + all_azure_projects: false, + azure_base_url: Url::parse("https://dev.azure.com/").unwrap(), + azure_repo_type: AzureRepoType::Source, + jira_url: None, + jql: None, + jira_include_comments: false, + jira_include_changelog: false, + confluence_url: None, + cql: None, + slack_query: None, + slack_api_url: Url::parse("https://slack.com/api/").unwrap(), + teams_query: None, + teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: true, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse(&format!("{}/", server.uri()))?, + max_results: 10, + s3_bucket: None, + s3_prefix: None, + role_arn: None, + aws_local_profile: None, + gcs_bucket: None, + gcs_prefix: None, + gcs_service_account: None, + docker_image: Vec::new(), + git_clone: GitCloneMode::Bare, + git_history: GitHistoryMode::Full, + commit_metadata: true, + repo_artifacts: false, + scan_nested_repos: true, + since_commit: None, + branch: None, + branch_root: false, + branch_root_commit: None, + staged: false, + }, + content_filtering_args: ContentFilteringArgs { + max_file_size_mb: 25.0, + extraction_depth: 2, + no_binary: true, + no_extract_archives: false, + exclude: Vec::new(), + }, + confidence: ConfidenceLevel::Low, + no_validate: true, + access_map: false, + rule_stats: false, + only_valid: false, + min_entropy: Some(0.0), + redact: false, + git_repo_timeout: 1800, + output_args: OutputArgs { output: None, format: ReportOutputFormat::Pretty }, + no_dedup: true, + baseline_file: None, + manage_baseline: false, + skip_regex: Vec::new(), + skip_word: Vec::new(), + skip_aws_account: Vec::new(), + skip_aws_account_file: None, + no_base64: false, + turbo: false, + extra_ignore_comments: Vec::new(), + no_inline_ignore: false, + no_ignore_if_contains: false, + view_report: false, + view_report_port: 7890, + view_report_address: "127.0.0.1".to_string(), + validation_retries: 1, + validation_rps: None, + validation_rps_rule: Vec::new(), + validation_timeout: 10, + full_validation_response: false, + max_validation_response_length: 2048, + }; + + let global_args = GlobalArgs { + verbose: 0, + quiet: true, + color: Mode::Auto, + no_update_check: false, + self_update: false, + progress: Mode::Never, + ignore_certs: false, + user_agent_suffix: None, + tls_mode: TlsMode::Strict, + allow_internal_ips: false, + endpoint: Vec::new(), + endpoint_config: None, + }; + + let loaded = RuleLoader::from_rule_specifiers(&scan_args.rules).load(&scan_args)?; + let resolved = loaded.resolve_enabled_rules()?; + let rules_db = Arc::new(RulesDatabase::from_rules(resolved.into_iter().cloned().collect())?); + + let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir))); + let update_status = UpdateStatus::default(); + + run_async_scan(&global_args, &scan_args, Arc::clone(&datastore), &rules_db, &update_status) + .await?; + + let ds = datastore.lock().unwrap(); + let findings = ds.get_matches().len(); + assert!( + findings >= 2, + "expected at least two findings (collection bearer + secret env value), got {}", + findings + ); + + // Both findings should resolve to a Postman web URL via the link map. + let postman_link_count = ds.postman_links().len(); + assert!( + postman_link_count >= 2, + "expected postman_links registered for collection + environment (got {})", + postman_link_count + ); + Ok(()) +} diff --git a/tests/int_redact.rs b/tests/int_redact.rs index fb0e01c..6353577 100644 --- a/tests/int_redact.rs +++ b/tests/int_redact.rs @@ -97,6 +97,12 @@ async fn test_redact_hashes_finding_values() -> Result<()> { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), s3_bucket: None, s3_prefix: None, role_arn: None, diff --git a/tests/int_slack.rs b/tests/int_slack.rs index d655297..4038177 100644 --- a/tests/int_slack.rs +++ b/tests/int_slack.rs @@ -101,6 +101,12 @@ impl TestContext { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), s3_bucket: None, s3_prefix: None, role_arn: None, @@ -262,6 +268,12 @@ async fn test_scan_slack_messages() -> Result<()> { slack_api_url: Url::parse(&format!("{}/", server.uri()))?, teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), max_results: 10, // s3 s3_bucket: None, diff --git a/tests/int_teams.rs b/tests/int_teams.rs index be9734c..ea97ed5 100644 --- a/tests/int_teams.rs +++ b/tests/int_teams.rs @@ -137,6 +137,12 @@ async fn test_scan_teams_messages() -> Result<()> { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: Some("secret".into()), teams_api_url: Url::parse(&format!("{}/", server.uri()))?, + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), max_results: 10, s3_bucket: None, s3_prefix: None, diff --git a/tests/int_validation_cache.rs b/tests/int_validation_cache.rs index 28ad0eb..7ec3071 100644 --- a/tests/int_validation_cache.rs +++ b/tests/int_validation_cache.rs @@ -174,6 +174,12 @@ async fn test_validation_cache_and_depvars() -> Result<()> { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), // s3 s3_bucket: None, s3_prefix: None, diff --git a/tests/int_vulnerable_files.rs b/tests/int_vulnerable_files.rs index f7a1056..cbb6878 100644 --- a/tests/int_vulnerable_files.rs +++ b/tests/int_vulnerable_files.rs @@ -117,6 +117,12 @@ impl TestContext { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), // s3 s3_bucket: None, s3_prefix: None, @@ -264,6 +270,12 @@ impl TestContext { slack_api_url: Url::parse("https://slack.com/api/").unwrap(), teams_query: None, teams_api_url: Url::parse("https://graph.microsoft.com/").unwrap(), + postman_workspaces: Vec::new(), + postman_collections: Vec::new(), + postman_environments: Vec::new(), + postman_all: false, + postman_include_mocks_monitors: false, + postman_api_url: Url::parse("https://api.getpostman.com/").unwrap(), // s3 s3_bucket: None, s3_prefix: None,