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

Diagram

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:

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

  1. Toolchain: Add opa test + conftest to your CI/CD pipeline
  2. Documentation: Every new security check = 1 Rego rule, not N Clojure functions
  3. Resume: Replace “Clojure security tooling” with “declarative policy engineering”
  4. 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.