This commit is contained in:
Luke Young 2026-04-17 23:25:02 -07:00
commit 6048462041
4 changed files with 140 additions and 20 deletions

1
Cargo.lock generated
View file

@ -5273,6 +5273,7 @@ name = "kingfisher-rules"
version = "0.1.0"
dependencies = [
"anyhow",
"base32",
"base64 0.22.1",
"crc32fast",
"hmac 0.12.1",

View file

@ -36,6 +36,7 @@ liquid = "0.26"
liquid-core = "0.26"
# Crypto for liquid filters
base32 = "0.5"
base64.workspace = true
crc32fast = "1.5"
hmac.workspace = true

View file

@ -4,9 +4,8 @@ rules:
pattern: |
(?x)
(
github_pat_
(?P<body>[0-9][A-Za-z0-9]{21}_[A-Za-z0-9]{43})
(?P<checksum>[A-Za-z0-9]{8})
(?P<body>github_pat_[0-9][A-Za-z0-9]{21}_[A-Za-z0-9]{43})
(?P<checksum>[A-Za-z2-7]{8})
[A-Za-z0-9]{8}
)
\b
@ -17,7 +16,7 @@ rules:
actual:
template: "{{ checksum }}"
requires_capture: checksum
expected: "{{ body | crc32 | base62: 8 }}"
expected: "{{ body | sha256_b32: 8 }}"
skip_if_missing: true
min_entropy: 3.5
examples:
@ -58,9 +57,9 @@ rules:
- name: GitHub Personal Access Token
id: kingfisher.github.2
pattern: |
(?xi)
(?x)
(
ghp_(?P<body>[A-Z0-9]{30})(?P<checksum>[A-Z0-9]{6})
ghp_(?P<body>[A-Za-z0-9]{30})(?P<checksum>[A-Za-z0-9]{6})
)
pattern_requirements:
min_digits: 2
@ -114,9 +113,9 @@ rules:
- name: GitHub OAuth Access Token
id: kingfisher.github.3
pattern: |
(?xi)
(?x)
(
gho_(?P<body>[A-Z0-9]{30})(?P<checksum>[A-Z0-9]{6})
gho_(?P<body>[A-Za-z0-9]{30})(?P<checksum>[A-Za-z0-9]{6})
)
pattern_requirements:
min_digits: 2
@ -149,18 +148,40 @@ rules:
words:
- '"login"'
- '"id"'
revocation:
type: Http
content:
request:
method: POST
url: https://api.github.com/credentials/revoke
headers:
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2022-11-28
Content-Type: application/json
body: '{"credentials":["{{ TOKEN }}"]}'
response_matcher:
- report_response: true
- type: StatusMatch
status: [202]
- name: GitHub App User-to-Server Token
id: kingfisher.github.4
pattern: |
(?xi)
(?x)
(
ghu_(?P<body>[A-Z0-9]{30})(?P<checksum>[A-Z0-9]{6})
ghu_(?P<body>[A-Za-z0-9]{30})(?P<checksum>[A-Za-z0-9]{6})
)
pattern_requirements:
checksum:
actual:
template: "{{ checksum }}"
requires_capture: checksum
expected: "{{ body | crc32 | base62: 6 }}"
skip_if_missing: true
examples:
- ' "token": "ghu_16C7e42F292c69C2E7C10c838347Ae178B4a",'
- ' "token": "ghu_TIOHHEVefAwRonSMALCFfWMYK0un1R1dj2rn",'
- |
Example usage:
git clone http://ghu_RguXIkihJjwHAP6eXEYxaPNvywurTr5IOAbg@github.com/username/repo.git
git clone http://ghu_imqBAXUtRirzzcJPwAiqImhkzsvzYZ1eDtPf@github.com/username/repo.git
references:
- https://docs.github.com/en/rest/users?apiVersion=2022-11-28
validation:
@ -179,15 +200,27 @@ rules:
words:
- '"login"'
- '"id"'
# Revocation not supported: ghu_ tokens require the GitHub App's client_id
# and client_secret (DELETE /applications/{client_id}/token with Basic auth).
# Users can revoke via GitHub Settings > Applications > Authorized GitHub Apps.
revocation:
type: Http
content:
request:
method: POST
url: https://api.github.com/credentials/revoke
headers:
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2022-11-28
Content-Type: application/json
body: '{"credentials":["{{ TOKEN }}"]}'
response_matcher:
- report_response: true
- type: StatusMatch
status: [202]
- name: GitHub App Server-to-Server Token
id: kingfisher.github.5
pattern: |
(?xi)
(?x)
(
ghs_(?P<body>[A-Z0-9]{30})(?P<checksum>[A-Z0-9]{6})
ghs_(?P<body>[A-Za-z0-9]{30})(?P<checksum>[A-Za-z0-9]{6})
)
examples:
- ' "token": "ghs_16C7e42F292c69C2E7C10c838347Ae178B4a",'
@ -228,12 +261,19 @@ rules:
- name: GitHub Refresh Token
id: kingfisher.github.6
pattern: |
(?xi)
(?x)
(
ghr_(?P<body>[A-Z0-9]{30})(?P<checksum>[A-Z0-9]{6})
ghr_(?P<body>[A-Za-z0-9]{70})(?P<checksum>[A-Za-z0-9]{6})
)
pattern_requirements:
checksum:
actual:
template: "{{ checksum }}"
requires_capture: checksum
expected: "{{ body | crc32 | base62: 6 }}"
skip_if_missing: true
examples:
- ' "refresh_token": "ghr_1B4a2e77838347a7E420ce178F2E7c6912E169246c3CE1ccbF66C46812d16D5B1A9Dc86A1498",'
- ' "refresh_token": "ghr_xgrrGzSbbGRL34Wp39JU9nxtN27Pr1v1He8FjE7x7wbExGGs7nfJszJDAmZuoKasxZ0KxJ1HSzgc",'
references:
- https://docs.github.com/en/rest/users?apiVersion=2022-11-28
validation:
@ -252,6 +292,21 @@ rules:
words:
- '"login"'
- '"id"'
revocation:
type: Http
content:
request:
method: POST
url: https://api.github.com/credentials/revoke
headers:
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2022-11-28
Content-Type: application/json
body: '{"credentials":["{{ TOKEN }}"]}'
response_matcher:
- report_response: true
- type: StatusMatch
status: [202]
- name: GitHub Client ID
id: kingfisher.github.7

View file

@ -540,6 +540,56 @@ static_filter!(
}
);
// {{ value | sha256_b32 }} -- base32-encoded SHA-256 digest, optional length
#[derive(Debug, FilterParameters)]
struct Sha256B32Args {
#[parameter(description = "Exact output length: truncates if longer, pads with '=' if shorter", arg_type = "integer")]
len: Option<Expression>,
}
#[derive(Clone, ParseFilter, FilterReflection, Default)]
#[filter(
name = "sha256_b32",
description = "SHA-256 digest encoded as Base32 (RFC 4648), optionally truncating or padding with '=' to an exact length.",
parameters(Sha256B32Args),
parsed(Sha256B32)
)]
pub struct Sha256B32Filter;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "sha256_b32"]
struct Sha256B32 {
#[parameters]
args: Sha256B32Args,
}
impl Filter for Sha256B32 {
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
let args = self.args.evaluate(runtime)?;
let mut h = Sha256::new();
h.update(input.to_kstr().as_bytes());
let mut encoded = base32::encode(
base32::Alphabet::Rfc4648 { padding: true },
&h.finalize()[..],
);
if let Some(len) = args.len.and_then(|value| {
let scalar = Value::scalar(value);
value_to_usize(&scalar)
}) {
match encoded.len().cmp(&len) {
std::cmp::Ordering::Greater => encoded.truncate(len),
std::cmp::Ordering::Less => {
for _ in 0..(len - encoded.len()) {
encoded.push('=');
}
}
std::cmp::Ordering::Equal => {}
}
}
Ok(Value::scalar(encoded))
}
}
static_filter!(
/// Compute the CRC32 of the input and return it as a decimal number.
Crc32Filter,
@ -1013,6 +1063,7 @@ pub fn register_all(builder: liquid::ParserBuilder) -> liquid::ParserBuilder {
.filter(Replace::default())
.filter(B64UrlEncFilter::default())
.filter(Sha256Filter::default())
.filter(Sha256B32Filter::default())
.filter(UrlEncodeFilter::default())
.filter(JsonEscapeFilter::default())
.filter(UnixTimestampFilter::default())
@ -1083,6 +1134,18 @@ mod tests {
assert_eq!(render(r#"{{ "hello" | sha256 }}"#), expect);
}
#[test]
fn sha256_b32_filter() {
let expect = "FTZE3OS7WCRQ4JXIHMVMLOPCTYNRMHS4D6TUEXTTAQZWFE4LTASA====";
assert_eq!(render(r#"{{ "hello" | sha256_b32 }}"#), expect);
// truncate
assert_eq!(render(r#"{{ "hello" | sha256_b32: 4 }}"#), &expect[..4]);
// pad
let padded = render(r#"{{ "hello" | sha256_b32: 60 }}"#);
assert!(padded.ends_with("===="));
assert_eq!(padded.len(), 60);
}
#[test]
fn suffix_filter() {
assert_eq!(render(r#"{{ "abcdef" | suffix: 3 }}"#), "def");