forked from mirrors/kingfisher
- Added kingfisher.temporal.1 rule for Temporal Cloud API keys (namespace-scoped and user-scoped JWT formats) with Temporal-specific pattern matching.
- Added Temporal Cloud active credential validation via GET https://saas-api.tmprl.cloud/cloud/current-identity using bearer auth, so Temporal keys validate against provider APIs instead of generic OIDC discovery. - Fixed JWT issuer normalization to treat bare host issuers (e.g. iss: temporal.io) as HTTPS URLs during discovery, avoiding low-level URL builder failures. - Added crates/kingfisher-rules/build.rs to ensure embedded rule assets rebuild when files under crates/kingfisher-rules/data change.
This commit is contained in:
parent
7dc0955635
commit
ec44d9b60b
5 changed files with 104 additions and 8 deletions
|
|
@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file.
|
|||
- Fixed HTTP validation incorrectly marking valid credentials as inactive when response bodies exceeded 2048 bytes. Matchers (`JsonValid`, `WordMatch`, etc.) now run against the full response; only the stored preview remains truncated for reporting.
|
||||
- Fixed validation flakiness under service rate limiting by retrying HTTP validations on 429/408 in addition to transient 5xx failures.
|
||||
- Prevented transient HTTP validation failures (429/5xx) from being cached, avoiding cache poisoning that could suppress later successful validations in the same scan.
|
||||
- Added `kingfisher.temporal.1` rule for Temporal Cloud API keys (namespace-scoped and user-scoped JWT formats) with Temporal-specific pattern matching.
|
||||
- Added Temporal Cloud active credential validation via `GET https://saas-api.tmprl.cloud/cloud/current-identity` using bearer auth, so Temporal keys validate against provider APIs instead of generic OIDC discovery.
|
||||
- Fixed JWT issuer normalization to treat bare host issuers (e.g. `iss: "temporal.io"`) as HTTPS URLs during discovery, avoiding low-level URL builder failures.
|
||||
- Added `crates/kingfisher-rules/build.rs` to ensure embedded rule assets rebuild when files under `crates/kingfisher-rules/data` change.
|
||||
|
||||
## [v1.81.0]
|
||||
- Fixed checksum-template evaluation for prefixed tokens by using explicit checksum/body captures in NPM, GitHub, Confluent, and GitLab rules.
|
||||
|
|
|
|||
|
|
@ -260,8 +260,8 @@ Kingfisher ships with [hundreds of rules](crates/kingfisher-rules/data/rules/) t
|
|||
| Category | What we catch |
|
||||
|----------|---------------|
|
||||
| **AI SaaS APIs** | OpenAI, Anthropic, Google Gemini, Cohere, Mistral, Stability AI, Replicate, xAI (Grok), Ollama, Langchain, Perplexity, Weights & Biases, Cerebras, Friendli, Fireworks.ai, NVIDIA NIM, together.ai, Zhipu, and more |
|
||||
| **Cloud Providers** | AWS, Azure, GCP, Alibaba Cloud, DigitalOcean, IBM Cloud, Cloudflare, and more |
|
||||
| **Dev & CI/CD** | GitHub/GitLab tokens, CircleCI, TravisCI, TeamCity, Docker Hub, npm, PyPI, and more |
|
||||
| **Cloud Providers** | AWS, Azure, GCP, Alibaba Cloud, DigitalOcean, IBM Cloud, Cloudflare, Temporal Cloud, and more |
|
||||
| **Dev & CI/CD** | GitHub/GitLab tokens, CircleCI, TravisCI, TeamCity, Docker Hub, npm, PyPI, Vercel, and more |
|
||||
| **Messaging & Comms** | Slack, Discord, Microsoft Teams, Twilio, Mailgun, SendGrid, Mailchimp, and more |
|
||||
| **Databases & Data Ops** | MongoDB Atlas, PlanetScale, Postgres DSNs, Grafana Cloud, Datadog, Dynatrace, and more |
|
||||
| **Payments & Billing** | Stripe, PayPal, Square, GoCardless, and more |
|
||||
|
|
|
|||
24
crates/kingfisher-rules/build.rs
Normal file
24
crates/kingfisher-rules/build.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let data_dir = Path::new("data");
|
||||
println!("cargo:rerun-if-changed={}", data_dir.display());
|
||||
emit_rerun_for_tree(data_dir);
|
||||
}
|
||||
|
||||
fn emit_rerun_for_tree(path: &Path) {
|
||||
let Ok(entries) = fs::read_dir(path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let p = entry.path();
|
||||
if p.is_dir() {
|
||||
emit_rerun_for_tree(&p);
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed={}", p.display());
|
||||
}
|
||||
}
|
||||
47
crates/kingfisher-rules/data/rules/temporal.yml
Normal file
47
crates/kingfisher-rules/data/rules/temporal.yml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
rules:
|
||||
- name: Temporal Cloud API Key
|
||||
id: kingfisher.temporal.1
|
||||
pattern: |
|
||||
(?x)
|
||||
\b
|
||||
(
|
||||
eyJ[A-Za-z0-9_-]{10,}
|
||||
\.
|
||||
[A-Za-z0-9_-]*Y2NvdW50X2lk (?# payload contains "account_id" )
|
||||
[A-Za-z0-9_-]*InRlbXBvcmFsLmlv (?# payload contains "temporal.io" )
|
||||
[A-Za-z0-9_-]*(?:ICJrZXlfaWQiOi|a2V5X2lk|rZXlfaWQi) (?# payload contains "key_id" )
|
||||
[A-Za-z0-9_-]{20,}
|
||||
\.
|
||||
[A-Za-z0-9_-]{20,}
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 3
|
||||
min_entropy: 3.2
|
||||
confidence: medium
|
||||
examples:
|
||||
- 'temporal_api_key="eyJhbGciOiJFUzI1NiIsImtpZCI6IlNhbXBsZSJ9.eyJhY2NvdW50X2lkIjoic2FtcGxlIiwiYXVkIjpbInRlbXBvcmFsLmlvIl0sImlzcyI6InRlbXBvcmFsLmlvIiwia2V5X2lkIjoic2FtcGxlLWtleSIsInN1YiI6InVzZXItMTIzIiwiZXhwIjoyMDAwMDAwMDAwfQ.c2lnbmF0dXJlX3BsYWNlaG9sZGVyXzEyMzQ1Njc4OTA"'
|
||||
- 'temporal --profile cloud config set --prop api_key --value "eyJhbGciOiJFUzI1NiIsImtpZCI6IkFub3RoZXJLZXkifQ.eyJhY2NvdW50X2lkIjoidGVzdC1hY2N0IiwiYXVkIjpbInRlbXBvcmFsLmlvIl0sImlzcyI6InRlbXBvcmFsLmlvIiwia2V5X2lkIjoidGVzdC1rZXktaWQiLCJzdWIiOiJ1c2VyLXRlc3QiLCJleHAiOjIwMDAwMDAwMDB9.c2lnbmF0dXJlX3Rlc3RfdmFsdWVfMDEyMzQ1Njc4OTAi'
|
||||
negative_examples:
|
||||
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInN1YiI6InVzZXIiLCJleHAiOjE5NzIxNzI0NjF9.WQWcwBAQFNE259f2o8ruFln_UMLTFEnEaUD7KHrs9Aw'
|
||||
references:
|
||||
- https://docs.temporal.io/cloud/api-keys
|
||||
- https://docs.temporal.io/cli/env-config
|
||||
validation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: GET
|
||||
url: https://saas-api.tmprl.cloud/cloud/current-identity
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Accept: application/json
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: WordMatch
|
||||
words:
|
||||
- '"user"'
|
||||
- '"serviceAccount"'
|
||||
match_all_words: false
|
||||
|
|
@ -162,7 +162,9 @@ pub async fn validate_jwt_with(
|
|||
return Ok((false, "no kid in header".into()));
|
||||
};
|
||||
|
||||
let config_url = format!("{}/.well-known/openid-configuration", issuer.trim_end_matches('/'));
|
||||
let issuer_url = normalize_issuer_url(&issuer)?;
|
||||
let config_url =
|
||||
format!("{}/.well-known/openid-configuration", issuer_url.as_str().trim_end_matches('/'));
|
||||
let cfg_resp = client
|
||||
.get(&config_url)
|
||||
.send()
|
||||
|
|
@ -186,11 +188,7 @@ pub async fn validate_jwt_with(
|
|||
return Ok((false, "jwks_uri must use https".to_string()));
|
||||
}
|
||||
|
||||
let iss_host = Url::parse(&issuer)
|
||||
.map_err(|e| anyhow!("invalid iss: {e}"))?
|
||||
.host_str()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
let iss_host = issuer_url.host_str().unwrap_or_default().to_ascii_lowercase();
|
||||
let jwks_host = url.host_str().unwrap_or_default().to_ascii_lowercase();
|
||||
if jwks_host != iss_host {
|
||||
return Ok((
|
||||
|
|
@ -245,3 +243,26 @@ fn extract_aud_strings(claims: &Claims) -> Vec<String> {
|
|||
fn is_blocked_ip(ip: std::net::IpAddr) -> bool {
|
||||
BLOCKED_NETS.iter().filter_map(|cidr| cidr.parse::<IpNet>().ok()).any(|net| net.contains(&ip))
|
||||
}
|
||||
|
||||
fn normalize_issuer_url(issuer: &str) -> Result<Url> {
|
||||
let trimmed = issuer.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("invalid iss: empty issuer"));
|
||||
}
|
||||
|
||||
if let Ok(url) = Url::parse(trimmed) {
|
||||
if url.host_str().is_some() {
|
||||
return Ok(url);
|
||||
}
|
||||
}
|
||||
|
||||
if !trimmed.contains("://") {
|
||||
let with_https = format!("https://{trimmed}");
|
||||
let url = Url::parse(&with_https).map_err(|e| anyhow!("invalid iss: {e}"))?;
|
||||
if url.host_str().is_some() {
|
||||
return Ok(url);
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("invalid iss: missing host"))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue