Added checksum to GitLab rule

This commit is contained in:
Mick Grove 2025-11-21 12:33:10 -08:00
commit ae01a24414
5 changed files with 111 additions and 7 deletions

View file

@ -2,6 +2,9 @@
All notable changes to this project will be documented in this file.
## [v1.67.0]
- Added checksum to GitLab rule
## [v1.66.0]
- Updating to support Bitbucket App Passwords
- Improved boundaries for several rules

View file

@ -10,7 +10,7 @@ publish = false
[package]
name = "kingfisher"
version = "1.66.0"
version = "1.67.0"
description = "MongoDB's blazingly fast and accurate secret scanning and validation tool"
edition.workspace = true
rust-version.workspace = true

View file

@ -120,21 +120,37 @@ rules:
- '"403 Forbidden"'
negative: true
url: https://gitlab.com/api/v4/ci/pipeline_triggers/{{ TOKEN }}
- name: GitLab Private Token - Updated Format
- name: GitLab Private Token - Routable Format
id: kingfisher.gitlab.4
pattern: |
(?x)
(?xi)
\b
(
glpat-[A-Za-z0-9_-]{36,38}\.01\.[a-z0-9]{9}
glpat-
(?<base64_payload>[0-9A-Za-z_-]{27,300})
\.
(?<version>01)
\.
(?<base36_payload_length>[0-9a-z]{2})
(?<crc32>[0-9a-z]{7})
)
\b
pattern_requirements:
min_digits: 2
# GitLab's RoutableTokenGenerator renders the CRC32 digest as lowercase
# base36 with a fixed width of 7 characters. The regex and checksum
# expectation mirror that encoding so we only report matches that carry a
# valid GitLab-style checksum.
checksum:
actual:
template: "{{ MATCH | suffix: 7 }}"
requires_capture: crc32
expected: "{{ \"glpat-\" | append: BASE64_PAYLOAD | append: \".01.\" | append: BASE36_PAYLOAD_LENGTH | crc32 | base36: 7 }}"
skip_if_missing: true
min_entropy: 3.5
confidence: medium
examples:
- glpat-5m8CwMZi4bwlRSCKzG0-3W86MQp1OmV5Y2UK.01.1012mzo24
- glpat-ymiBP0-I-J6ghspoBPoZxtSC3g7MyHYG0X0r.01.101erjmwl
references:
- https://github.com/diffblue/gitlab/blob/39c63ee83369bf5353256a6b95f3116728edd102/doc/api/personal_access_tokens.md
- https://docs.gitlab.com/api/personal_access_tokens/
@ -150,4 +166,4 @@ rules:
- type: WordMatch
words:
- '"id"'
url: https://gitlab.com/api/v4/personal_access_tokens/self
url: https://gitlab.com/api/v4/personal_access_tokens/self

View file

@ -20,8 +20,9 @@ rules:
ignore_if_contains:
- "*****"
- "xxxxx"
- "username:"
min_entropy: 4.0
confidence: medium
confidence: low
examples:
- https://username:secret@example.com/path
validation:

View file

@ -728,6 +728,83 @@ fn value_to_usize(value: &Value) -> Option<usize> {
.or_else(|| view.to_kstr().parse::<usize>().ok())
}
#[derive(Debug, FilterParameters)]
struct Base36Args {
#[parameter(
description = "Pad the encoded value to at least this width",
arg_type = "integer"
)]
width: Option<Expression>,
}
#[derive(Clone, ParseFilter, FilterReflection, Default)]
#[filter(
name = "base36",
description = "Encode the provided integer value using Base36.",
parameters(Base36Args),
parsed(Base36)
)]
pub struct Base36Filter;
#[derive(Debug, FromFilterParameters, Display_filter)]
#[name = "base36"]
struct Base36 {
#[parameters]
args: Base36Args,
}
impl Filter for Base36 {
fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
let args = self.args.evaluate(runtime)?;
let value = input
.as_scalar()
.and_then(|scalar| {
if let Some(int) = scalar.to_integer() {
Some(if int < 0 { 0 } else { int as u64 })
} else if let Some(float) = scalar.to_float() {
Some(if float.is_sign_negative() { 0 } else { float.floor() as u64 })
} else if let Some(boolean) = scalar.to_bool() {
Some(u64::from(boolean))
} else {
scalar.to_kstr().to_string().parse::<u64>().ok()
}
})
.or_else(|| input.to_kstr().to_string().parse::<u64>().ok())
.unwrap_or(0);
let mut encoded = encode_base36(value);
if let Some(width) = args.width.and_then(|value| {
let scalar = Value::scalar(value);
value_to_usize(&scalar)
}) {
if encoded.len() < width {
let mut padded = String::with_capacity(width);
for _ in 0..(width - encoded.len()) {
padded.push('0');
}
padded.push_str(&encoded);
encoded = padded;
}
}
Ok(Value::scalar(encoded))
}
}
fn encode_base36(mut value: u64) -> String {
const ALPHABET: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
if value == 0 {
return "0".to_string();
}
let mut buf = Vec::new();
while value > 0 {
let rem = (value % 36) as usize;
buf.push(ALPHABET[rem] as char);
value /= 36;
}
buf.iter().rev().collect()
}
// {{ value | b64url_enc }} URL-safe base64 w/o padding
static_filter!(
/// Base64 URL-safe (no = padding).
@ -844,6 +921,7 @@ pub fn register_all(builder: liquid::ParserBuilder) -> liquid::ParserBuilder {
.filter(Crc32HexFilter::default())
.filter(Crc32LeB64Filter::default())
.filter(Base62Filter::default())
.filter(Base36Filter::default())
.filter(HmacSha256::default())
.filter(HmacSha1::default())
.filter(HmacSha384::default())
@ -911,6 +989,12 @@ mod tests {
assert_eq!(render(r#"{{ "hello" | crc32 | base62: 6 }}"#), "0zNvy2");
}
#[test]
fn base36_filter() {
assert_eq!(render(r#"{{ 123456 | base36 }}"#), "2n9c");
assert_eq!(render(r#"{{ 123456 | base36: 6 }}"#), "002n9c");
}
#[test]
fn crc32_dec_filter() {
assert_eq!(render(r#"{{ "hello" | crc32_dec }}"#), "907060870");