diff --git a/CHANGELOG.md b/CHANGELOG.md index 60adaf1..50d8a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 2776297..1e48257 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/data/rules/gitlab.yml b/data/rules/gitlab.yml index 96a98bc..ad1a1e7 100644 --- a/data/rules/gitlab.yml +++ b/data/rules/gitlab.yml @@ -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- + (?[0-9A-Za-z_-]{27,300}) + \. + (?01) + \. + (?[0-9a-z]{2}) + (?[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 \ No newline at end of file + url: https://gitlab.com/api/v4/personal_access_tokens/self diff --git a/data/rules/uri.yml b/data/rules/uri.yml index 45fde52..db03edc 100644 --- a/data/rules/uri.yml +++ b/data/rules/uri.yml @@ -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: diff --git a/src/liquid_filters.rs b/src/liquid_filters.rs index 66a2fab..d334360 100644 --- a/src/liquid_filters.rs +++ b/src/liquid_filters.rs @@ -728,6 +728,83 @@ fn value_to_usize(value: &Value) -> Option { .or_else(|| view.to_kstr().parse::().ok()) } +#[derive(Debug, FilterParameters)] +struct Base36Args { + #[parameter( + description = "Pad the encoded value to at least this width", + arg_type = "integer" + )] + width: Option, +} + +#[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 { + 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::().ok() + } + }) + .or_else(|| input.to_kstr().to_string().parse::().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");