kingfisher/tests/int_postman.rs
2026-05-28 13:54:59 -07:00

316 lines
11 KiB
Rust

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(),
docker_archive: 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,
alert_webhook: Vec::new(),
alert_format: None,
alert_on: kingfisher::alerts::AlertOn::Findings,
alert_min_confidence: ConfidenceLevel::Medium,
alert_include_secret: false,
alert_report_url: None,
alert_detail: kingfisher::alerts::AlertDetail::Auto,
config_webhook_overrides: Vec::new(),
};
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,
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,
false,
)
.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(())
}