updated confluent rule with a checksum. Added zuplo rule with a checksum

This commit is contained in:
Mick Grove 2025-11-08 16:01:58 -08:00
commit 1ee9e804b0
4 changed files with 306 additions and 1 deletions

View file

@ -52,6 +52,43 @@ rules:
- 200
type: StatusMatch
url: https://api.confluent.cloud/iam/v2/api-keys/{{ CLIENTID }}
depends_on_rule:
- rule_id: "kingfisher.confluent.1"
variable: CLIENTID
- name: Confluent API Secret - Updated Format
id: kingfisher.confluent.3
pattern: |
(?xi)
\b
(
cflt(?P<body>[A-Za-z0-9\+/]{54})(?P<checksum>[A-Za-z0-9\+/]{6})
)
pattern_requirements:
checksum:
actual:
template: "{{ MATCH | suffix: 6 }}"
requires_capture: checksum
expected: "{{ BODY | crc32_le_b64: 6 }}"
skip_if_missing: true
min_entropy: 3.3
confidence: medium
examples:
- confluent secret=cfltqPLd2lLPAtWtHGNhN32WlZxoEj30pcg8mzaPlPJ937JlMa7n9YCRLooqgifw
references:
- https://docs.confluent.io/cloud/current/api.html#tag/API-Keys-(iamv2)/operation/getIamV2ApiKey
validation:
type: Http
content:
request:
headers:
Authorization: 'Basic {{ CLIENTID | append: ":" | append: TOKEN | b64enc }}'
method: GET
response_matcher:
- report_response: true
- status:
- 200
type: StatusMatch
url: https://api.confluent.cloud/iam/v2/api-keys/{{ CLIENTID }}
depends_on_rule:
- rule_id: "kingfisher.confluent.1"
variable: CLIENTID

22
data/rules/zuplo.yml Normal file
View file

@ -0,0 +1,22 @@
rules:
- name: Zuplo API Key
id: kingfisher.zuplo.1
pattern: |
(?xi)
\b
(
zpka_(?P<body>[a-z0-9]{32})_(?P<checksum>[0-9a-f]{8})
)
pattern_requirements:
checksum:
actual:
template: "{{ CHECKSUM | downcase }}"
requires_capture: checksum
expected: "{{ BODY | crc32_hex }}"
min_entropy: 3.3
confidence: medium
examples:
- zpka_3e6c4f7d39954ca29353b7ab88589b64_de26cd55
- zpka_b3f94d8d3d4d4a6ea5c5b20d0a5bb407_18eb262b
references:
- https://zuplo.com/blog/api-key-authentication

View file

@ -119,11 +119,15 @@ Below is the complete list of Liquid filters available in Kingfisher, along with
| `b64url_enc` | | URL-safe Base64 (no padding). Useful for JWT headers & payloads. | `{{ TOKEN \| b64url_enc }}` |
| `b64dec` | | Decodes a Base64 string. | `{{ "aGVsbG8=" \| b64dec }}` |
| `sha256` | | Computes the SHA-256 hex digest of the input. | `{{ TOKEN \| sha256 }}` |
| `crc32` | | Computes the CRC32 checksum of the input and returns a decimal value. | `{{ TOKEN \| crc32 }}` |
| `crc32` | | Computes the CRC32 checksum of the input and returns a decimal value. | `{{ TOKEN \| crc32 }}` |
| `crc32_dec` | `digits` (integer, optional) | Computes the CRC32 checksum and returns the last `digits` decimal characters (zero-padded). Defaults to the full value when omitted. | `{{ TOKEN \| crc32_dec: 6 }}` |
| `crc32_hex` | `digits` (integer, optional) | Computes the CRC32 checksum and returns the last `digits` hexadecimal characters (zero-padded). Defaults to the full value when omitted. | `{{ TOKEN \| crc32_hex: 8 }}` |
| `crc32_le_b64` | `len` (integer, optional) | Computes the CRC32 checksum, encodes the little-endian bytes using Base64, and optionally truncates to the first `len` characters. | `{{ TOKEN \| crc32_le_b64: 6 }}` |
| `hmac_sha1` | `key` (string) | Computes HMAC-SHA1 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha1: "secret-key" }}` |
| `hmac_sha256` | `key` (string) | Computes HMAC-SHA256 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha256: "secret-key" }}` |
| `hmac_sha384` | `key` (string) | Computes HMAC-SHA384 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha384: "secret-key" }}` |
| `random_string` | `len` (integer, optional) | Generates a cryptographically-secure random alphanumeric string of the specified length (default: 32). | `{{ "" \| random_string: 16 }}` |
| `prefix` | `len` (integer, optional) | Returns the first `len` characters from the string (default: full). | `{{ TOKEN \| prefix: 6 }}` |
| `suffix` | `len` (integer, optional) | Returns the last `len` characters from the string (default: full). | `{{ TOKEN \| suffix: 6 }}` |
| `base62` | `width` (integer, optional) | Encodes the input number as Base62, left-padding with zeros as needed. | `{{ TOKEN \| crc32 \| base62: 6 }}` |
| `url_encode` | | Percent-encodes the input according to RFC 3986. | `{{ TOKEN \| url_encode }}` |

View file

@ -309,6 +309,49 @@ impl Filter for Suffix {
}
}
#[derive(Debug, FilterParameters)]
struct PrefixArgs {
#[parameter(description = "Number of leading characters to keep", arg_type = "integer")]
len: Option<Expression>,
}
#[derive(Clone, ParseFilter, FilterReflection, Default)]
#[filter(
name = "prefix",
description = "Return the prefix (first N characters) of the provided string.",
parameters(PrefixArgs),
parsed(Prefix)
)]
pub struct PrefixFilter;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "prefix"]
struct Prefix {
#[parameters]
args: PrefixArgs,
}
impl Filter for Prefix {
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
let args = self.args.evaluate(runtime)?;
let text = input.to_kstr();
let requested = args
.len
.and_then(|value| {
let scalar = Value::scalar(value);
value_to_usize(&scalar)
})
.unwrap_or_else(|| text.len());
if requested == 0 {
return Ok(Value::scalar(String::new()));
}
let mut chars: Vec<char> = text.chars().collect();
chars.truncate(requested.min(chars.len()));
Ok(Value::scalar(chars.into_iter().collect::<String>()))
}
}
#[derive(Debug, Clone, Default, FilterReflection, ParseFilter)]
#[filter(
name = "b64enc",
@ -387,6 +430,175 @@ static_filter!(
}
);
#[derive(Debug, FilterParameters)]
struct Crc32DecArgs {
#[parameter(
description = "Number of trailing decimal digits to return (zero padded)",
arg_type = "integer"
)]
digits: Option<Expression>,
}
#[derive(Clone, ParseFilter, FilterReflection, Default)]
#[filter(
name = "crc32_dec",
description = "Compute the CRC32 and optionally return the last N decimal digits.",
parameters(Crc32DecArgs),
parsed(Crc32Dec)
)]
pub struct Crc32DecFilter;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "crc32_dec"]
struct Crc32Dec {
#[parameters]
args: Crc32DecArgs,
}
impl Filter for Crc32Dec {
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
let args = self.args.evaluate(runtime)?;
let mut hasher = Hasher::new();
hasher.update(input.to_kstr().as_bytes());
let checksum = u128::from(hasher.finalize());
let digits = args
.digits
.and_then(|value| {
let scalar = Value::scalar(value);
value_to_usize(&scalar)
})
.unwrap_or(0);
if digits == 0 {
return Ok(Value::scalar(checksum.to_string()));
}
let clamped_digits = digits.min(38); // 10^38 fits within u128
let modulus = 10u128.pow(clamped_digits as u32);
let truncated = checksum % modulus;
let mut value = truncated.to_string();
if clamped_digits > value.len() {
let mut padded = String::with_capacity(clamped_digits);
for _ in 0..(clamped_digits - value.len()) {
padded.push('0');
}
padded.push_str(&value);
value = padded;
}
Ok(Value::scalar(value))
}
}
#[derive(Debug, FilterParameters)]
struct Crc32HexArgs {
#[parameter(
description = "Number of trailing hexadecimal digits to return (zero padded)",
arg_type = "integer"
)]
digits: Option<Expression>,
}
#[derive(Clone, ParseFilter, FilterReflection, Default)]
#[filter(
name = "crc32_hex",
description = "Compute the CRC32 and optionally return the last N hexadecimal digits.",
parameters(Crc32HexArgs),
parsed(Crc32Hex)
)]
pub struct Crc32HexFilter;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "crc32_hex"]
struct Crc32Hex {
#[parameters]
args: Crc32HexArgs,
}
impl Filter for Crc32Hex {
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
let args = self.args.evaluate(runtime)?;
let mut hasher = Hasher::new();
hasher.update(input.to_kstr().as_bytes());
let checksum = hasher.finalize();
let mut hex = format!("{checksum:08x}");
let digits = args
.digits
.and_then(|value| {
let scalar = Value::scalar(value);
value_to_usize(&scalar)
})
.unwrap_or(0);
if digits == 0 {
return Ok(Value::scalar(hex));
}
let clamped = digits.min(32);
if clamped > hex.len() {
let mut padded = String::with_capacity(clamped);
for _ in 0..(clamped - hex.len()) {
padded.push('0');
}
padded.push_str(&hex);
hex = padded;
} else {
let start = hex.len() - clamped;
hex = hex[start..].to_string();
}
Ok(Value::scalar(hex))
}
}
#[derive(Debug, FilterParameters)]
struct Crc32LeB64Args {
#[parameter(
description = "Number of leading characters from the Base64 string to keep",
arg_type = "integer"
)]
len: Option<Expression>,
}
#[derive(Clone, ParseFilter, FilterReflection, Default)]
#[filter(
name = "crc32_le_b64",
description = "Compute the CRC32, encode little-endian bytes as Base64, optionally truncating.",
parameters(Crc32LeB64Args),
parsed(Crc32LeB64)
)]
pub struct Crc32LeB64Filter;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "crc32_le_b64"]
struct Crc32LeB64 {
#[parameters]
args: Crc32LeB64Args,
}
impl Filter for Crc32LeB64 {
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
let args = self.args.evaluate(runtime)?;
let mut hasher = Hasher::new();
hasher.update(input.to_kstr().as_bytes());
let checksum = hasher.finalize();
let encoded = general_purpose::STANDARD.encode(checksum.to_le_bytes());
let output = if let Some(len) = args.len.and_then(|value| {
let scalar = Value::scalar(value);
value_to_usize(&scalar)
}) {
encoded.chars().take(len).collect::<String>()
} else {
encoded
};
Ok(Value::scalar(output))
}
}
#[derive(Debug, FilterParameters)]
struct Base62Args {
#[parameter(
@ -590,7 +802,11 @@ pub fn register_all(builder: liquid::ParserBuilder) -> liquid::ParserBuilder {
.filter(B64DecFilter::default())
.filter(RandomStringFilter::default())
.filter(SuffixFilter::default())
.filter(PrefixFilter::default())
.filter(Crc32Filter::default())
.filter(Crc32DecFilter::default())
.filter(Crc32HexFilter::default())
.filter(Crc32LeB64Filter::default())
.filter(Base62Filter::default())
.filter(HmacSha256::default())
.filter(HmacSha1::default())
@ -645,6 +861,13 @@ mod tests {
assert_eq!(render(r#"{{ "value" | suffix: 0 }}"#), "");
}
#[test]
fn prefix_filter() {
assert_eq!(render(r#"{{ "abcdef" | prefix: 3 }}"#), "abc");
assert_eq!(render(r#"{{ "short" | prefix: 10 }}"#), "short");
assert_eq!(render(r#"{{ "value" | prefix: 0 }}"#), "");
}
#[test]
fn crc32_and_base62_filters() {
assert_eq!(render(r#"{{ "hello" | crc32 }}"#), "907060870");
@ -652,6 +875,25 @@ mod tests {
assert_eq!(render(r#"{{ "hello" | crc32 | base62: 6 }}"#), "0zNvy2");
}
#[test]
fn crc32_dec_filter() {
assert_eq!(render(r#"{{ "hello" | crc32_dec }}"#), "907060870");
assert_eq!(render(r#"{{ "hello" | crc32_dec: 6 }}"#), "060870");
}
#[test]
fn crc32_hex_filter() {
assert_eq!(render(r#"{{ "hello" | crc32_hex }}"#), "3610a686");
assert_eq!(render(r#"{{ "hello" | crc32_hex: 4 }}"#), "a686");
assert_eq!(render(r#"{{ "hello" | crc32_hex: 10 }}"#), "003610a686");
}
#[test]
fn crc32_le_b64_filter() {
assert_eq!(render(r#"{{ "hello" | crc32_le_b64 }}"#), "hqYQNg==");
assert_eq!(render(r#"{{ "hello" | crc32_le_b64: 6 }}"#), "hqYQNg");
}
#[test]
fn hmac_sha1_filter() {
let key = b"key1";