From e5ec170ac15dcca8df559c1028f3313a105da9b7 Mon Sep 17 00:00:00 2001 From: Adam Pastierik Date: Wed, 25 Mar 2026 14:29:37 +0100 Subject: [PATCH 1/4] Port OpenAI, Groq and GitLab PAT analyzers to rego --- patterns/leaktk/1/opa_policy.rego | 90 +++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/patterns/leaktk/1/opa_policy.rego b/patterns/leaktk/1/opa_policy.rego index e8b94954..04e769e3 100644 --- a/patterns/leaktk/1/opa_policy.rego +++ b/patterns/leaktk/1/opa_policy.rego @@ -184,3 +184,93 @@ analyzed_findings contains analyzed_finding if { "token": finding.secret, })}) } + +# OpenAI API Keys +analyzed_findings contains analyzed_finding if { + some finding in findings + contains(lower(finding.rule.description), "openai") + regex.match(`^sk-[\w\-]{16,}T3BlbkFJ[\w\-]{16,}$`, finding.secret) + resp := http.send({ + "url": "https://api.openai.com/v1/models", + "method": "GET", + "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, + }) + analyzed_finding := object.union(finding, { + "valid": resp.status_code == 200, + "analysis": {"status_code": resp.status_code}, + }) +} + +# Groq API Keys +analyzed_findings contains analyzed_finding if { + some finding in findings + contains(lower(finding.rule.description), "groq") + regex.match(`^gsk_[A-Za-z0-9]{52}$`, finding.secret) + resp := http.send({ + "url": "https://api.groq.com/openai/v1/models", + "method": "GET", + "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, + }) + analyzed_finding := object.union(finding, { + "valid": resp.status_code == 200, + "analysis": {"status_code": resp.status_code}, + }) +} + +# GitLab Personal Access Tokens +analyzed_findings contains analyzed_finding if { + some finding in findings + contains(lower(finding.rule.description), "gitlab") + contains(lower(finding.rule.description), "personal") + regex.match(`^glpat-[\w\-]{20}$|^glpat-[\w\-]{32,235}\.[0-9a-z]{2}\.[0-9a-z]{9}$`, finding.secret) + token_resp := http.send({ + "url": "https://gitlab.com/api/v4/personal_access_tokens/self", + "method": "GET", + "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, + }) + + # GitLab can return 200 with active:false for expired tokens, so we need to check both the status code and the active field + token_resp.status_code == 200 + token_resp.body.active == true + + user_id := token_resp.body.user_id + + # Second request to get user details for the token owner + user_resp := http.send({ + "url": sprintf("https://gitlab.com/api/v4/users/%d", [user_id]), + "method": "GET", + "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, + }) + + analyzed_finding := object.union(finding, { + "valid": true, + "analysis": { + "token_name": token_resp.body.name, + "scopes": token_resp.body.scopes, + "expires_at": token_resp.body.expires_at, + "user_id": user_id, + "username": user_resp.body.username, + "is_admin": user_resp.body.is_admin, + }, + }) +} + +# GitLab Personal Access Tokens +# Fallback rule to explicitly mark invalid/expired tokens as valid: false +# rather than falling through to unanalyzed_findings +analyzed_findings contains analyzed_finding if { + some finding in findings + contains(lower(finding.rule.description), "gitlab") + contains(lower(finding.rule.description), "personal") + regex.match(`^glpat-[\w\-]{20}$|^glpat-[\w\-]{32,235}\.[0-9a-z]{2}\.[0-9a-z]{9}$`, finding.secret) + token_resp := http.send({ + "url": "https://gitlab.com/api/v4/personal_access_tokens/self", + "method": "GET", + "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, + }) + token_resp.status_code != 200 + analyzed_finding := object.union(finding, { + "valid": false, + "analysis": {"status_code": token_resp.status_code}, + }) +} \ No newline at end of file From aace93a97dcd5237fb152d2be5a71c8f54b85835 Mon Sep 17 00:00:00 2001 From: "adam.pastierik" <113629042+Eteriss@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:52:47 +0200 Subject: [PATCH 2/4] Apply suggestion from @bplaxco Co-authored-by: Braxton Plaxco --- patterns/leaktk/1/opa_policy.rego | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/leaktk/1/opa_policy.rego b/patterns/leaktk/1/opa_policy.rego index 04e769e3..a0c638d9 100644 --- a/patterns/leaktk/1/opa_policy.rego +++ b/patterns/leaktk/1/opa_policy.rego @@ -212,7 +212,7 @@ analyzed_findings contains analyzed_finding if { "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, }) analyzed_finding := object.union(finding, { - "valid": resp.status_code == 200, + "valid": valid_status_code(resp.status_code, [200, 200], [400, 499]), "analysis": {"status_code": resp.status_code}, }) } From 86c8b49da7241be2e0edead920da151d97ed3dda Mon Sep 17 00:00:00 2001 From: "adam.pastierik" <113629042+Eteriss@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:53:16 +0200 Subject: [PATCH 3/4] Apply suggestion from @bplaxco Co-authored-by: Braxton Plaxco --- patterns/leaktk/1/opa_policy.rego | 77 +++++++++---------------------- 1 file changed, 23 insertions(+), 54 deletions(-) diff --git a/patterns/leaktk/1/opa_policy.rego b/patterns/leaktk/1/opa_policy.rego index a0c638d9..b817c43a 100644 --- a/patterns/leaktk/1/opa_policy.rego +++ b/patterns/leaktk/1/opa_policy.rego @@ -219,58 +219,27 @@ analyzed_findings contains analyzed_finding if { # GitLab Personal Access Tokens analyzed_findings contains analyzed_finding if { - some finding in findings - contains(lower(finding.rule.description), "gitlab") - contains(lower(finding.rule.description), "personal") - regex.match(`^glpat-[\w\-]{20}$|^glpat-[\w\-]{32,235}\.[0-9a-z]{2}\.[0-9a-z]{9}$`, finding.secret) - token_resp := http.send({ - "url": "https://gitlab.com/api/v4/personal_access_tokens/self", - "method": "GET", - "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, - }) - - # GitLab can return 200 with active:false for expired tokens, so we need to check both the status code and the active field - token_resp.status_code == 200 - token_resp.body.active == true - - user_id := token_resp.body.user_id - - # Second request to get user details for the token owner - user_resp := http.send({ - "url": sprintf("https://gitlab.com/api/v4/users/%d", [user_id]), - "method": "GET", - "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, - }) - - analyzed_finding := object.union(finding, { - "valid": true, - "analysis": { - "token_name": token_resp.body.name, - "scopes": token_resp.body.scopes, - "expires_at": token_resp.body.expires_at, - "user_id": user_id, - "username": user_resp.body.username, - "is_admin": user_resp.body.is_admin, - }, - }) -} - -# GitLab Personal Access Tokens -# Fallback rule to explicitly mark invalid/expired tokens as valid: false -# rather than falling through to unanalyzed_findings -analyzed_findings contains analyzed_finding if { - some finding in findings - contains(lower(finding.rule.description), "gitlab") - contains(lower(finding.rule.description), "personal") - regex.match(`^glpat-[\w\-]{20}$|^glpat-[\w\-]{32,235}\.[0-9a-z]{2}\.[0-9a-z]{9}$`, finding.secret) - token_resp := http.send({ - "url": "https://gitlab.com/api/v4/personal_access_tokens/self", - "method": "GET", - "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, - }) - token_resp.status_code != 200 - analyzed_finding := object.union(finding, { - "valid": false, - "analysis": {"status_code": token_resp.status_code}, - }) + some finding in findings + contains(lower(finding.rule.description), "gitlab") + regex.match(`^glpat-[\w\-]{20}$|^glpat-[\w\-]{32,235}\.[0-9a-z]{2}\.[0-9a-z]{9}$`, finding.secret) + + user_resp := http.send({ + "url": "https://gitlab.com/api/v4/user", + "method": "GET", + "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, + }) + + token_resp := http.send({ + "url": "https://gitlab.com/api/v4/personal_access_tokens/self", + "method": "GET", + "headers": {"Authorization": sprintf("Bearer %s", [finding.secret])}, + }) + + analyzed_finding := object.union(finding, { + "valid": user_resp.status_code == 200, + "analysis": { + "user": get_if(user_resp, "body", user_resp.status_code == 200), + "token": get_if(token_resp, "body", token_resp.status_code == 200), + }, + }) } \ No newline at end of file From 5d6df31f931f6d26ca82ff98bdcd4c7a7a96d853 Mon Sep 17 00:00:00 2001 From: Adam Pastierik Date: Mon, 30 Mar 2026 10:04:58 +0200 Subject: [PATCH 4/4] Add valid_status_code and get_if helper functions --- patterns/leaktk/1/opa_policy.rego | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/patterns/leaktk/1/opa_policy.rego b/patterns/leaktk/1/opa_policy.rego index b817c43a..8f734406 100644 --- a/patterns/leaktk/1/opa_policy.rego +++ b/patterns/leaktk/1/opa_policy.rego @@ -22,6 +22,23 @@ auth_bearer_token_valid(opts) if { }).status_code < 300 } +default valid_status_code(status_code, valid_range, invalid_range) := null + +valid_status_code(status_code, valid_range, invalid_range) := false if { + status_code >= invalid_range[0] + status_code <= invalid_range[1] +} + +valid_status_code(status_code, valid_range, invalid_range) := true if { + status_code >= valid_range[0] + status_code <= valid_range[1] +} + +# Helper for accessing a field if a condition is true +default get_if(object, field, condition) := null + +get_if(object, field, condition) := object[field] if condition + container_registry_auth_opts(hostname) := opts if { lower(hostname) == "docker.io" opts := {