RSS Amplifier

Looking at Computer · May 21, 2026

Express YourCELf: Filtering and Validating Secrets with CEL

0
Sign in to vote or save

Zachary Rice · Looking at Computer

UPDATE: 6-26-26

In Betterleaks v1.6.0, we switched the expression runtime from CEL to Expr. The overall model described in this post still holds: Betterleaks uses embedded expressions for prefilters, filters, and validation logic. The language backing those expressions has changed.

The short version is that CEL was a good fit conceptually, but Expr ended up being a better fit operationally for Betterleaks. Expr gave us a smaller, simpler runtime for the kind of expressions Betterleaks needs. The change is not really a rejection of CEL as an idea. CEL is still a great language for many embedded policy systems. Existing CEL-shaped Betterleaks configs are still accepted through a compatibility layer, but new configs should use Expr syntax going forward. For example, where older examples use CEL optional-map access like: attributes[?”path”].orValue(”“) new configs should prefer: get(attributes, “path”, “”). And where older examples use cel.bind, expr can use let, like:

let r = http.get(”https://api.github.com/user”, {
“Authorization”: “token “ + secret
});
r.status == 200 ? {
“result”: “valid”
} : validate.unknown(r)
So when this post says “CEL,” mentally read that as “Betterleaks expressions.” The product idea stayed the same; the implementation moved to Expr because it made the expressions more readable, binary faster to start, and resulted in a smaller binary.

Would you take a look at those expressions!?

I’ve spent the last several years working on secret detection as the author and maintainer of Gitleaks. Gitleaks showed that regex, entropy, and rule-based filtering are useful foundations for finding candidate secrets, but it also showed where that model becomes difficult to extend.

Filtering in Gitleaks was handled through allowlists, which are just TOML tables that tell the scanner when to ignore a finding. They worked for common cases like ignoring test fixtures, example values, or known false positives, but they became awkward as the logic became more specific. Paths, regexes, stopwords, match targets, and conditions were all modeled as separate TOML fields, which made more nuanced filtering possible but not especially pleasant.

Gitleaks also stops at detection and filtering. It does not provide a general validation system. Once a candidate secret is found, there is no built-in way to ask the provider whether that credential is still usable. Validation often requires provider-specific behavior via HTTP requests, signed payloads, timestamps, custom headers, and response parsing.

Betterleaks is my attempt to take what worked in Gitleaks and clean up the parts that got awkward over time. For validation and filtering, that meant moving some of the logic out of fixed TOML fields and into CEL, Google’s Common Expression Language.

CEL is a small expression language designed to be embedded inside larger applications. It gives users a constrained way to express conditions against data the application provides. There are some widely used projects already using CEL like Kubernetes, Envoy, and gRPC.

Below is a CEL expression from the kubernetes docs validating that two sets are disjoint.

Betterleaks uses CEL as a way to extend the declarative TOML config that defines rules, regexes, keywords, identifiers, and other static configuration. CEL handles the parts of a rule that need more expressive logic like deciding whether a resource should be scanned, deciding whether a candidate finding should be ignored, or making a validation request and interpreting the response.

If you go to the cel.dev website you’ll see the question “Is CEL right for your project”. It goes on to state, “CEL is ideal for performance-critical applications because it was designed to evaluate safely and quickly (nanoseconds to microseconds) with predictable costs. CEL expressions are especially useful for predicate logic and simple data transformations. CEL is used most efficiently in applications where expressions are evaluated frequently, but modified infrequently.” This description matches the shape of how Betterleaks approaches secrets scanning. In Betterleaks, expressions are compiled once when needed, then evaluated repeatedly across resources and candidate findings during the filtering and validation stages of a scan.

In release v1.1.0 we introduced a change that lets you wire up validation logic in CEL. A Betterleaks validator returns one of 6 outcomes: valid, needs validation, invalid, revoked, error, or unknown. Valid means the credential worked. Needs validation means validation wasn’t attempted but there is ample evidence for manual validation. Invalid means the provider rejected it in a way we understand. Revoked means the provider recognized the credential, but indicated that it can no longer be used. Error means an error occurred during evaluation. Unknown means Betterleaks could not safely classify the result.

Now let’s look at some examples.

Below is a CEL expression for validating a GitHub App token. Note that it’s a tad simplified for demonstration purposes.

cel.bind is a macro used to assign an intermediate value. In this case, the response from GitHub is bound to r, and the rest of the expression classifies the credential based on the response status. A 200 response produces a valid result. A 401 or 403 response produces an invalid result. Anything else produces an unknown result.

Some providers require more than a bearer token in a single request. Some validation flows depend on timestamps, secondary captured values, derived headers, or cryptographic signatures. CEL lets us express that request construction and response classification without adding provider-specific fields to the TOML schema.

This validator for Polymarket uses values captured by other rules (polymarket-api-secret, polymarket-passphrase), computes a timestamped HMAC signature, sends the validation request, then classifies the response.

CEL also supports application-specific functions. Betterleaks exposes general-purpose helpers such as crypto.hmac_sha256, but some providers are better handled with a dedicated function. AWS validation is a good example because the request requires SigV4 signing, which is useful behavior to centralize rather than repeat in every rule.

aws.validate does the provider-specific work: it builds a SigV4-signed request to STS (Security Token Service) GetCallerIdentity and returns the response as a CEL value. The expression still controls how that response is interpreted. On success, it can return AWS identity metadata such as ARN, account, and user ID.

Secret scanners excel at finding secrets in source code. They use a combination of regular expressions, entropy-based searches and other heuristics. That body of work is well defined and easy to implement. The challenge is taking a huge list of candidate secrets and distilling it down into a manageable size list of findings to review.

When I wrote Gitleaks, we used an allowlist to filter out obvious false positives, like secrets with the word ‘EXAMPLE’ in them. But this felt super clunky. Take a look at Gitleaks’ allowlist for generic secrets below:

There are a few things happening here. The first allowlist checks the captured secret against a regex and suppresses values that look like ordinary words or identifiers. The second allowlist changes the target to the full match, checks that match against another set of regexes, and also suppresses findings when the captured secret contains one of several stopwords. None of that logic is especially complicated, but it is spread across several TOML fields with behavior that depends on field names like regexTarget, regexes, and stopwords. As these cases accumulate, the configuration starts to feel less like a rule and more like a small filtering language implemented through TOML tables.

CEL makes that filtering logic explicit. Instead of encoding the decision across multiple allowlist fields, the rule can express the condition directly as a boolean expression. If the expression returns true, Betterleaks drops the finding. The same example translated to CEL looks like this:`

We use matchesAny instead of the built-in CEL matches function. matchesAny is a custom function that compiles all the patterns from a list into one regex and uses whichever regex engine betterleaks is configured to use (stdlib or re2).

There are two kinds of filters in Betterleaks, prefilters and filters. Prefilters run during resource enumeration and let you bail out early before hitting potentially expensive regex operations. Prefilters have access to resource metadata (we call this metadata, Attributes). Filters have access to resource metadata like prefilters, but also have access to candidate finding data. Betterleaks configs carry one top-level prefilter that applies to every fragment, one top-level filter that applies to all candidate findings, and rule-level filters that apply filters to only the candidates triggered by that rule.

A good example of a prefilter would be skipping image files, or dependabot commits:

Or say we wanted to exclude scanning release notes and artifacts from all v1.x.x releases in a repo called “sillyrepo”. We could wire up a top level prefilter to look like this:

Filters runs after a regex match and sees both the resource metadata (attributes) and the candidate finding itself. Filters can be placed at the top level (apply this filter to every candidate) or at the rule level (apply this filter only against candidates w/ matching this rule). This is where you can express things like “if this fails the TokenEfficiency test and was committed before June 2nd 2025, ignore this finding”.

Most rule level filters are entropy and token efficiency checks:

The overall model is fairly simple. TOML defines the rule, and CEL extends the rule where validation or filtering logic is needed.

Ffewaf
Rough Betterleaks flowchart and example config (also did you know you can copy mermaid charts directly into tldraw.com?? Pretty cool huh?

In Betterleaks, CEL is used to express provider-specific validation behavior, metadata-based prefilters, and finding-level suppression logic. This replaces several implicit configuration patterns with explicit boolean and structured expressions. But don’t worry, your Gitleaks configs with allowlists will work just fine with Betterleaks. There’s a translation layer that translates allowlists to CEL filters.

Thanks for reading Looking at Computer! This post is public so feel free to share it.

Share

No posts

Read the original on lookingatcomputer.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.