forked from mirrors/kingfisher
Added validation for Alibaba rule
This commit is contained in:
parent
cd4f626502
commit
dcb2191fe8
5 changed files with 184 additions and 17 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:**
|
||||
|
|
|
|||
|
|
@ -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 there’s 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue