Why I Replaced 200 Lines of Babashka with 35 Lines of Rego
Opening Hook
Last Tuesday I rewrote 200 lines of Clojure security checks into 35 lines of Rego. My first reaction was embarrassment — until I realized I’d just applied 50 years of programming language theory I already knew.
The Before/After
| Metric | Babashka Clojure | OPA/Rego |
|---|---|---|
| Lines of code | ~200 | 35 |
| Execution model | Imperative scan loops | Declarative rule evaluation |
| Test coverage | 50 tests / 175 assertions in the original tool | Requires separate opa test cases and adapter
fixtures |
| Input contract | Application-shaped Clojure data | Normalized policy input supplied by an adapter |
The intended decision logic stayed narrow, but the migration still required an explicit input contract and new fixtures. Rego changes the policy representation; it does not remove the adapter or integration work.
What Actually Changed
Old Approach: Imperative Clojure
(defn check-iam-role [role]
(let [statements (:statements role)
violations (atom [])]
(doseq [stmt statements]
(when (= (:effect stmt) "Allow")
(when (some #(= % "*") (:action stmt))
(swap! violations conj
(format "Role %s has wildcard action" (:name role))))
(when (some #(= % "*") (:resource stmt))
(swap! violations conj
(format "Role %s has wildcard resource" (:name role)))))
@violations))
This example mixes traversal, decision logic, and result accumulation in one function. Its ordering, fixtures, and test behavior are properties of the application code, not automatic guarantees of Clojure.
New Approach: Declarative Rego
# Conceptual custom checks over a normalized Terraform-plan input.
# The adapter maps the plan into input.aws_iam_roles, where each role has
# an inline_policies array and each policy has a Statement array.
package terraform.aws.iam.role
import rego.v1
# This is a custom wildcard-action check; it is not Checkov CKV2_AWS_5.
deny contains msg if {
some role in input.aws_iam_roles
some policy in role.inline_policies
some statement in policy.Statement
statement.Effect == "Allow"
some action in statement.Action
action == "*"
msg := sprintf("[IAM-WILDCARD] Role '%v' allows Action='*' — use scoped managed policies", [role.name])
}
# Custom wildcard-resource check.
deny contains msg if {
some role in input.aws_iam_roles
some policy in role.inline_policies
some statement in policy.Statement
statement.Effect == "Allow"
some resource in statement.Resource
resource == "*"
msg := sprintf("[IAM-RESOURCE] Role '%v' allows Resource='*' — scope to specific ARNs", [role.name])
}
# Custom inline-policy governance check.
deny contains msg if {
some role in input.aws_iam_roles
count(role.inline_policies) > 0
msg := sprintf("[IAM-INLINE] Role '%v' uses an inline policy — review managed-policy migration", [role.name])
}
Benefits: - Declarative constraints: Rules define
what must hold, not an application-side scan loop. -
Explicit data contract: The policy expects a normalized
input shape; the Terraform-plan adapter remains part of the system. -
Set-valued decisions: deny contains msg
returns a set of messages, with duplicates removed by set semantics. -
Built-in testing: opa test evaluates
policy tests; it does not replace assertions about the adapter,
fixtures, or deployment pipeline.
Functional vs Declarative Security Models
Why Functional Programmers Already Understand This
| Clojure Concept | Policy Engine Parallel |
|---|---|
| EDN as code | Facts are immutable data |
| Core.logic | Rules define relationships declaratively |
| Rule evaluation | Declarative query over facts |
| REPL debugging | opa eval shows intermediate states |
| Pure functions | Rules return violations, don’t mutate state |
The practical difference is the policy toolchain: OPA provides evaluation, testing, and integration points, while the surrounding application still owns input normalization and deployment.
Business Impact
For my migration experiment, the useful comparison is scope rather than a universal performance claim:
- Policy surface: 35 lines for the three illustrative rules versus roughly 200 lines of prior scan code.
- Decision model: the policy returns violations from input facts instead of mutating an accumulator.
- Auditability: the rule intent is visible in the policy file, while the adapter and fixtures still require separate review.
The line count and any measured false-positive or timing change are project-specific results, not properties guaranteed by OPA.
The Resume Translation
Don’t say “rewrote Clojure in Rego”. Say:
Architected declarative IAM validation using OPA/Rego and a normalized Terraform-plan input; replaced an imperative scan path with set-valued deny decisions and testable policy rules.
This maps directly to Senior SecOps Engineer — Policy-as-Code job requirements.
Action Items
- Toolchain: Add
opa test+conftestto your CI/CD pipeline - Documentation: Every new security check = 1 Rego rule, not N Clojure functions
- Resume: Replace “Clojure security tooling” with “declarative policy engineering”
- LinkedIn: Post about “replacing custom security code with OPA — same logic, better execution”
Takeaway
You didn’t abandon functional programming.
You applied a familiar functional habit — separating data from transformation — in a policy language built for authorization and compliance decisions.
Rego is not Clojure’s replacement or logical conclusion. It is a distinct language with its own semantics, runtime, input contracts, and operational tradeoffs.