forked from mirrors/kingfisher
Added first-class **Postman** scanning target: new kingfisher scan postman subcommand (and equivalent --postman-* flags) fetches workspaces, collections, and environments via the Postman API and scans them for hard-coded credentials in request auth blocks, pre-request/test scripts, saved example responses, and — notably — secret-typed environment variables, which the API returns in plaintext despite the UI mask. Selectors: --workspace, --collection, --environment, --all, with optional --include-mocks-monitors and --api-url for self-hosted endpoints. Authenticates via KF_POSTMAN_TOKEN (or POSTMAN_API_KEY) sent as X-Api-Key; honors X-RateLimit-RetryAfter on 429s. Findings link back to https://go.postman.co/... URLs in reports.
This commit is contained in:
parent
0b89e4b02f
commit
997480ffc7
39 changed files with 1613 additions and 184 deletions
|
|
@ -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<PathBuf>,
|
||||
|
||||
/// Optional path to write an interactive D3.js HTML report
|
||||
#[clap(long, value_name = "PATH")]
|
||||
pub html_out: Option<PathBuf>,
|
||||
#[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<PathBuf>,
|
||||
#[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<PathBuf>,
|
||||
|
||||
/// 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<Box<dyn std::io::Write>> {
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// Scan a single Postman environment by UID (repeatable)
|
||||
#[arg(long = "postman-environment", value_name = "UID", hide = true)]
|
||||
pub postman_environments: Vec<String>,
|
||||
|
||||
/// 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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// Scan a single Postman environment by UID (repeatable)
|
||||
#[arg(long = "environment", alias = "postman-environment", value_name = "UID")]
|
||||
pub environments: Vec<String>,
|
||||
|
||||
/// 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue