Added validation for Alibaba rule

This commit is contained in:
Mick Grove 2025-07-09 15:03:07 -07:00
commit dcb2191fe8
5 changed files with 184 additions and 17 deletions

View file

@ -6,7 +6,9 @@ All notable changes to this project will be documented in this file.
## [1.20.0]
- Removed confirmation prompt when user provides --self-update flag
- Added support for HTTP request bodies in rule validation
- Added new liquid-rs filters: HmacSha1, IsoTimestampNoFracFilter, Replace
- Added rules for mistral, perplexity
- Added validation for Alibaba rule
## [1.19.0]
- JSON output was missing committer name and email

View file

@ -10,6 +10,7 @@ rules:
\b
min_entropy: 4.0
confidence: medium
visible: false
examples:
- LTAI8x2NiGqfyJGx7eLDhp12
- LTAI5GqyJGhp12ad31L5hpix
@ -36,22 +37,22 @@ rules:
request:
method: GET
url: >
{% assign nonce = "" | uuid | url_encode -%}
{% assign ts = "" | iso_timestamp | url_encode -%}
{% capture qs -%}
AccessKeyId={{ AKID }}&
Action=GetCallerIdentity&
Format=JSON&
SignatureMethod=HMAC-SHA1&
SignatureNonce={{ nonce }}&
SignatureVersion=1.0&
Timestamp={{ ts }}&
Version=2015-04-01
{%- endcapture %}
{% capture sts -%}GET&%2F&{{ qs | url_encode }}{%- endcapture %}
{% assign key = TOKEN | append: '&' -%}
{% assign sig = sts | hmac_sha256: key | b64enc | url_encode -%}
https://sts.aliyuncs.com/?{{ qs }}&Signature={{ sig }}
{%- assign nonce = "" | uuid | upcase -%}
{%- assign raw_timestamp = "" | iso_timestamp_no_frac -%}
{%- assign timestamp = raw_timestamp | replace: ":", "%3A" -%}
{%- capture params -%}
AccessKeyId={{ AKID | url_encode }}&Action=GetCallerIdentity&Format=JSON&SignatureMethod=HMAC-SHA1&SignatureNonce={{ nonce }}&SignatureVersion=1.0&Timestamp={{ timestamp }}&Version=2015-04-01
{%- endcapture -%}
{%- assign encoded_params = params | replace: "+", "%20" | replace: "*", "%2A" | replace: "%7E", "~" -%}
{%- assign query_string = encoded_params | url_encode | replace: "%2D", "-" | replace: "%2E", "." -%}
{%- assign signature_base_string = "GET&%2F&" | append: query_string -%}
{%- assign token_amp = TOKEN | append: "&" -%}
{%- assign hmacsignature = signature_base_string | hmac_sha1: token_amp | url_encode -%}
https://sts.aliyuncs.com/?{{ params }}&Signature={{ hmacsignature }}
headers:
Accept: application/json
response_matcher:
@ -60,7 +61,6 @@ rules:
status: [200]
- type: WordMatch
words: ['"Arn"']
match_all_words: true
depends_on_rule:
- rule_id: kingfisher.alibabacloud.1
variable: AKID

View file

@ -84,7 +84,51 @@ rules:
| **XmlValid** | | Pass only if body parses as well-formed XML. Use when response is expected as XML data |
| **ReportResponse** | `report_response` (bool) | Include raw payload in finding for debugging. |
## 2. Templating with Liquid
Kingfisher leverages the Liquid template engine for dynamic parts of HTTP request bodies, headers, query parameters, and multipart payloads. The engine supports both built-in and custom filters to manipulate the captured secret (TOKEN) or other named captures ({{ NAME }}).
### Using Liquid Filters in Validation
- **Capture Injection**: The unnamed capture from your regex becomes {{ TOKEN }}. Named captures are made available as uppercase variables (e.g. {{ RDMVAL }}).
- **Filter Pipeline**: You can chain filters using the pipe (|) syntax:
```liquid
{{ TOKEN | b64enc | url_encode }}
```
Arguments: Some filters accept parameters, provided after a colon:
```liquid
{{ TOKEN | hmac_sha256: "my-secret-key" }}
```
### 3. Built-in & Custom Liquid Filters
Below is the complete list of Liquid filters available in Kingfisher, along with their usage patterns and examples.
| Filter | Parameters | Description | Example |
| --------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `b64enc` | | Base64-encodes the input using the standard alphabet. | `{{ TOKEN \| b64enc }}` |
| `b64url_enc` | | URL-safe Base64 (no padding). Useful for JWT headers & payloads. | `{{ TOKEN \| b64url_enc }}` |
| `sha256` | | Computes the SHA-256 hex digest of the input. | `{{ TOKEN \| sha256 }}` |
| `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 }}` |
| `url_encode` | | Percent-encodes the input according to RFC 3986. | `{{ TOKEN \| url_encode }}` |
| `json_escape` | | Escapes special characters so a string can be safely injected into JSON contexts. | `{{ TOKEN \| json_escape }}` |
| `unix_timestamp` | | Returns the current Unix epoch time in seconds (UTC). | `{{ "" \| unix_timestamp }}` |
| `iso_timestamp` | | Returns the current UTC timestamp in full ISO-8601 format (may include fractional seconds). | `{{ "" \| iso_timestamp }}` |
| `iso_timestamp_no_frac` | | Current ISO-8601 timestamp (UTC) **without** fractional seconds. | `{{ "" \| iso_timestamp_no_frac }}` |
| `uuid` | | Generates a random UUIDv4 string. | `{{ "" \| uuid }}` |
| `jwt_header` | | Builds a minimal JWT header JSON (`{"typ":"JWT","alg":…}`) and Base64URL-encodes it. | `{{ "HS256" \| jwt_header }}` |
| `replace` | `from` (string), `to` (string) | Replaces every occurrence of `from` with `to` in the input string. | `{{ "hello world" \| replace: "world", "mars" }}` |
**Chaining & Composition:** Filters can be stacked; e.g.:
```liquid
Authorization: Basic {{ "api:" | append: TOKEN | b64enc }}
```
**Runtime Values:** Filters like unix_timestamp and uuid are evaluated at runtime, enabling nonces, timestamps, and unique IDs in your requests.
### How depends_on_rule Works
- **Dependency Declaration:**

View file

@ -9,6 +9,7 @@ use liquid_core::{
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use rand::{distr::Alphanumeric, Rng};
use sha2::{Digest, Sha256, Sha384};
use sha1::Sha1;
use time::{format_description::well_known::Iso8601, OffsetDateTime};
use uuid::Uuid;
@ -72,6 +73,40 @@ macro_rules! static_filter {
};
}
#[derive(Debug, FilterParameters)]
struct ReplaceArgs {
#[parameter(description = "The substring to search for.", arg_type = "str")]
from: Expression,
#[parameter(description = "The string to replace it with.", arg_type = "str")]
to: Expression,
}
#[derive(Clone, ParseFilter, FilterReflection, Default)]
#[filter(
name = "replace",
description = "Replaces every occurrence of a substring with another.",
parameters(ReplaceArgs),
parsed(ReplaceFilter)
)]
pub struct Replace;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "replace"]
struct ReplaceFilter {
#[parameters]
args: ReplaceArgs,
}
impl Filter for ReplaceFilter {
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
let args = self.args.evaluate(runtime)?;
let from = args.from.to_kstr();
let to = args.to.to_kstr();
let input_str = input.to_kstr();
Ok(Value::scalar(input_str.replace(from.as_str(), to.as_str())))
}
}
// ── HMAC args ─────────────────────────────────────
#[derive(Debug, FilterParameters)]
struct HmacArgs {
@ -110,6 +145,45 @@ impl Filter for HmacSha256Filter {
}
}
// ── HMAC-SHA1 ─────────────────────────────────────────────
#[derive(Debug, FilterParameters)]
struct Hmac1Args {
#[parameter(description = "HMAC key", arg_type = "str")]
key: Expression,
}
#[derive(Clone, ParseFilter, FilterReflection, Default)]
#[filter(
name = "hmac_sha1",
description = "HMAC-SHA1 returns Base64.",
parameters(Hmac1Args),
parsed(HmacSha1Filter)
)]
pub struct HmacSha1;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "hmac_sha1"]
struct HmacSha1Filter {
#[parameters]
args: Hmac1Args,
}
impl Filter for HmacSha1Filter {
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
// Evaluate the arguments first…
let args = self.args.evaluate(runtime)?;
let key = args.key.to_kstr();
// …then do the cryptography.
let mut mac = Hmac::<Sha1>::new_from_slice(key.as_bytes()).unwrap();
mac.update(input.to_kstr().as_bytes());
Ok(Value::scalar(
base64::engine::general_purpose::STANDARD.encode(mac.finalize().into_bytes()),
))
}
}
// ── HMAC-SHA384 ─────────────────────────────────────────────
#[derive(Debug, FilterParameters)]
struct Hmac384Args {
@ -260,6 +334,26 @@ static_filter!(
}
);
// {{ "" | iso_timestamp_no_frac }}
static_filter!(
/// Current ISO-8601 timestamp (UTC) with no fractional seconds.
IsoTimestampNoFracFilter, "iso_timestamp_no_frac",
|_input: &dyn ValueView| -> String {
let full = OffsetDateTime::now_utc()
.format(&Iso8601::DEFAULT)
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".into());
// If theres a fractional-second part, remove it but keep the trailing Z.
match full.split_once('.') {
Some((prefix, _)) => {
format!("{prefix}Z")
}
None => full,
}
}
);
// {{ "" | iso_timestamp }}
static_filter!(
/// Current ISO-8601 timestamp (UTC).
@ -285,17 +379,20 @@ static_filter!(
pub fn register_all(builder: liquid::ParserBuilder) -> liquid::ParserBuilder {
builder
// zero-arg helpers
.filter(Replace::default())
.filter(B64UrlEncFilter::default())
.filter(Sha256Filter::default())
.filter(UrlEncodeFilter::default())
.filter(JsonEscapeFilter::default())
.filter(UnixTimestampFilter::default())
.filter(IsoTimestampFilter::default())
.filter(IsoTimestampNoFracFilter::default())
.filter(UuidFilter::default())
.filter(JwtHeaderFilter::default())
.filter(B64EncFilter::default())
.filter(RandomStringFilter::default())
.filter(HmacSha256::default())
.filter(HmacSha1::default())
.filter(HmacSha384::default())
}
@ -308,6 +405,7 @@ mod tests {
use regex::Regex;
use sha2::{Digest, Sha256, Sha384};
use time::OffsetDateTime;
use sha1::Sha1;
use super::*;
@ -334,6 +432,17 @@ mod tests {
assert_eq!(render(r#"{{ "hello" | sha256 }}"#), expect);
}
#[test]
fn hmac_sha1_filter() {
let key = b"key1";
let data = b"data";
let mut mac = Hmac::<Sha1>::new_from_slice(key).unwrap();
mac.update(data);
let expect = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
assert_eq!(render(r#"{{ "data" | hmac_sha1: "key1" }}"#), expect);
}
#[test]
fn b64url_enc_filter() {
assert_eq!(
@ -416,6 +525,16 @@ mod tests {
assert!((now - tmpl_val).abs() < 5, "timestamp differs by >5 s");
}
#[test]
fn rfc3986_ts_filter_format() {
let ts = render(r#"{{ "" | rfc3986_ts }}"#);
// RFC-3986 form: 2025-07-09T03%3A36%3A40Z
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}%3A\d{2}%3A\d{2}Z$").unwrap();
assert!(re.is_match(&ts), "timestamp not RFC3986-encoded: {ts}");
assert!(!ts.contains('.'), "sub-seconds should be removed: {ts}");
}
#[test]
fn iso_timestamp_filter_parses() {
let out = render(r#"{{ "" | iso_timestamp }}"#);
@ -434,4 +553,5 @@ mod tests {
let v = render(r#"{{ "" | uuid }}"#);
assert!(uuid_re.is_match(&v));
}
}

View file

@ -527,4 +527,5 @@ mod tests {
// 4⃣ It *should* be valid (true) because all matcher conditions hold
assert!(ok, "Slack webhook response should be considered ACTIVE");
}
}