diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b5ed7..df09a58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ All notable changes to this project will be documented in this file. - Added live HTTP validation for Etsy, JFrog, Octopus Deploy, OpenShift, and Private AI where provider documentation supported reliable token-only checks. - Added detection + validation rules for Anthropic Admin, Azure Speech, Azure Translator, Databento, DataStax Astra, DevCycle, Fullstory, GC Notify, and Stytch; built-in runtime rule count is now 601 with `--confidence=low`. - Added Heroku token revocation support for both legacy UUID-format tokens and `HRKU-` platform tokens via the OAuth authorizations API. +- Added `hmac_sha256_b64key` Liquid filter for HMAC-SHA256 signing with base64-encoded keys (decodes key to raw bytes before signing), enabling correct Azure Notification Hub SAS validation. +- Integrated SLSA v3 provenance generation into the release workflow; hash computation now scopes to build artifacts only for idempotent re-runs. +- Removed Zapier webhook live validation (GET to a catch hook triggers the Zap). +- Hardened Heroku revocation regex to prevent crossing JSON object boundaries when extracting authorization IDs. +- Fixed Zendesk subdomain regex to reject trailing hyphens; renamed `ZENDESK_SUBDOMAIN` to `ZENDESK_HOST` for clarity. +- Fixed Stytch and Polymarket trailing `\b` boundaries that prevented matching base64-padded secrets ending with `=`. +- Tightened Kubernetes API Server URL pattern to require kube-specific identifiers, preventing bootstrap tokens from binding to unrelated `server:` entries. ## [v1.91.0] - Added SSRF protection for credential validation: outbound HTTP requests now block connections to loopback, private, link-local, and other non-public IP addresses. HTTP redirect targets are DNS-resolved and validated against the same SSRF rules. Use `--allow-internal-ips` to opt out when scanning internal infrastructure. diff --git a/crates/kingfisher-rules/data/rules/azure-notification-hub.yml b/crates/kingfisher-rules/data/rules/azure-notification-hub.yml index 42be867..6b40af1 100644 --- a/crates/kingfisher-rules/data/rules/azure-notification-hub.yml +++ b/crates/kingfisher-rules/data/rules/azure-notification-hub.yml @@ -91,20 +91,15 @@ rules: (?:notification\s*hub|Endpoint\s*=\s*sb://[a-z0-9-]{2,63}\.servicebus\.windows\.net/?) (?:.|[\n\r]){0,160}? SharedAccessKey - \s*[:=]\s* - ["']? - ( - [A-Za-z0-9+/]{32,88}={0,2} - ) | \b (?:hubAccessKey|notificationHub(?:Access)?Key) \b - \s*[:=]\s* - ["']? - ( - [A-Za-z0-9+/]{32,88}={0,2} - ) + ) + \s*[:=]\s* + ["']? + ( + [A-Za-z0-9+/]{32,88}={0,2} ) ["']? (?:[^A-Za-z0-9+/=]|$) @@ -149,8 +144,7 @@ rules: {%- assign se = "" | unix_timestamp | plus: 300 -%} {%- capture to_sign -%}{{ uri | url_encode }} {{ se }}{%- endcapture -%} - {%- assign key_bytes = TOKEN | b64dec -%} - {%- capture auth -%}SharedAccessSignature sr={{ uri | url_encode }}&sig={{ to_sign | hmac_sha256: key_bytes | url_encode }}&se={{ se }}&skn={{ NH_KEY_NAME | url_encode }}{%- endcapture -%} + {%- capture auth -%}SharedAccessSignature sr={{ uri | url_encode }}&sig={{ to_sign | hmac_sha256_b64key: TOKEN | url_encode }}&se={{ se }}&skn={{ NH_KEY_NAME | url_encode }}{%- endcapture -%} {{ auth | strip_newlines }} response_matcher: - report_response: true diff --git a/crates/kingfisher-rules/src/liquid_filters.rs b/crates/kingfisher-rules/src/liquid_filters.rs index 16a8e8b..351678b 100644 --- a/crates/kingfisher-rules/src/liquid_filters.rs +++ b/crates/kingfisher-rules/src/liquid_filters.rs @@ -182,6 +182,45 @@ impl Filter for HmacSha256Filter { } } +// ── HMAC-SHA256 with base64-encoded key ────────────────────────────────── +#[derive(Debug, FilterParameters)] +struct HmacB64KeyArgs { + #[parameter(description = "Base64-encoded HMAC key", arg_type = "str")] + key: Expression, +} + +#[derive(Clone, ParseFilter, FilterReflection, Default)] +#[filter( + name = "hmac_sha256_b64key", + description = "HMAC-SHA256 with a base64-encoded key – decodes the key to raw bytes before signing. Returns Base64.", + parameters(HmacB64KeyArgs), + parsed(HmacSha256B64KeyFilter) +)] +pub struct HmacSha256B64Key; + +#[derive(Debug, FromFilterParameters, Display_filter)] +#[name = "hmac_sha256_b64key"] +struct HmacSha256B64KeyFilter { + #[parameters] + args: HmacB64KeyArgs, +} + +impl Filter for HmacSha256B64KeyFilter { + fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result { + let args = self.args.evaluate(runtime)?; + let key_b64 = args.key.to_kstr(); + + let key_bytes = general_purpose::STANDARD.decode(key_b64.as_bytes()).map_err(|e| { + LiquidError::with_msg(format!("hmac_sha256_b64key: invalid base64 key: {e}")) + })?; + + let mut mac = Hmac::::new_from_slice(&key_bytes) + .map_err(|e| LiquidError::with_msg(format!("hmac_sha256_b64key: {e}")))?; + mac.update(input.to_kstr().as_bytes()); + Ok(Value::scalar(general_purpose::STANDARD.encode(mac.finalize().into_bytes()))) + } +} + // ── HMAC-SHA1 ───────────────────────────────────────────── #[derive(Debug, FilterParameters)] struct HmacSha1Args { @@ -923,6 +962,7 @@ pub fn register_all(builder: liquid::ParserBuilder) -> liquid::ParserBuilder { .filter(Base62Filter::default()) .filter(Base36Filter::default()) .filter(HmacSha256::default()) + .filter(HmacSha256B64Key::default()) .filter(HmacSha1::default()) .filter(HmacSha384::default()) } @@ -1073,6 +1113,21 @@ mod tests { assert_eq!(render(r#"{{ "hi!" | hmac_sha256: "secret" }}"#), expect); } + #[test] + fn hmac_sha256_b64key_filter() { + // Key is base64-encoded; the filter must decode it to raw bytes before HMAC. + let raw_key: &[u8] = &[0x00, 0x80, 0xFF, 0x42, 0xDE, 0xAD, 0xBE, 0xEF]; + let b64_key = general_purpose::STANDARD.encode(raw_key); + + let data = b"hello azure"; + let mut mac = Hmac::::new_from_slice(raw_key).unwrap(); + mac.update(data); + let expect = general_purpose::STANDARD.encode(mac.finalize().into_bytes()); + + let template = format!(r#"{{{{ "hello azure" | hmac_sha256_b64key: "{b64_key}" }}}}"#); + assert_eq!(render(&template), expect); + } + #[test] fn hmac_sha384_filter() { let key = b"topsecret";