diff --git a/Cargo.lock b/Cargo.lock
index b5ccddf..f0886bd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5273,6 +5273,7 @@ name = "kingfisher-rules"
version = "0.1.0"
dependencies = [
"anyhow",
+ "base32",
"base64 0.22.1",
"crc32fast",
"hmac 0.12.1",
diff --git a/crates/kingfisher-rules/Cargo.toml b/crates/kingfisher-rules/Cargo.toml
index 420247a..4e4772a 100644
--- a/crates/kingfisher-rules/Cargo.toml
+++ b/crates/kingfisher-rules/Cargo.toml
@@ -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
diff --git a/crates/kingfisher-rules/data/rules/github.yml b/crates/kingfisher-rules/data/rules/github.yml
index 484da40..fa2e3cd 100644
--- a/crates/kingfisher-rules/data/rules/github.yml
+++ b/crates/kingfisher-rules/data/rules/github.yml
@@ -4,9 +4,8 @@ rules:
pattern: |
(?x)
(
- github_pat_
- (?P
[0-9][A-Za-z0-9]{21}_[A-Za-z0-9]{43})
- (?P[A-Za-z0-9]{8})
+ (?Pgithub_pat_[0-9][A-Za-z0-9]{21}_[A-Za-z0-9]{43})
+ (?P[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[A-Z0-9]{30})(?P[A-Z0-9]{6})
+ ghp_(?P[A-Za-z0-9]{30})(?P[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[A-Z0-9]{30})(?P[A-Z0-9]{6})
+ gho_(?P[A-Za-z0-9]{30})(?P[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[A-Z0-9]{30})(?P[A-Z0-9]{6})
+ ghu_(?P[A-Za-z0-9]{30})(?P[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[A-Z0-9]{30})(?P[A-Z0-9]{6})
+ ghs_(?P[A-Za-z0-9]{30})(?P[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[A-Z0-9]{30})(?P[A-Z0-9]{6})
+ ghr_(?P[A-Za-z0-9]{70})(?P[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
diff --git a/crates/kingfisher-rules/src/liquid_filters.rs b/crates/kingfisher-rules/src/liquid_filters.rs
index cf6b73e..19322cd 100644
--- a/crates/kingfisher-rules/src/liquid_filters.rs
+++ b/crates/kingfisher-rules/src/liquid_filters.rs
@@ -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,
+}
+
+#[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 {
+ 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");