hopeless/labs

Blog

Why we wrote a language instead of another plugin API

jeagercoder1 min read
designrsl

Most scanners start the same way. A small engine, a handful of built-in checks, and then a request: "can I add my own?" So the project grows a plugin API. Then the plugin API grows a loader, a version negotiation layer, and a config file that describes which plugins run in which order.

Two years later nobody can answer a simple question — what does this scanner actually send to my target? — without reading three files and one README.

A check should be readable on its own

Ruso's answer is a small script format, RSL. A check describes probes, the traffic it sends, and the condition that turns a response into a finding:

name "Exposed Redis"
severity high

tcp redis { host "{{scan_host}}" port 6379 }

send redis
redis.write "PING\r\n"
match redis.response contains "+PONG"   # no auth required

There is no registration step, no lifecycle callbacks, no hidden state. What you read is what runs.

Boring on purpose

The DSL is deliberately small. If a check needs something the language cannot express, that is a conversation about the language — not a reason to bolt on an escape hatch that only two people understand. Small languages stay reviewable, and reviewable tooling is the whole point of a security scanner.

What this buys you

  • Diffable checks. A new detection is a small text diff in a pull request.
  • Portable artifacts. The same script runs locally, in CI, and from the registry — the compiler emits bytecode, the runtime executes it.
  • Honest reviews. A reviewer can see every byte a check sends.

The trade-off is real: a DSL cannot do everything a general-purpose language can. We think that constraint is a feature. If you want to argue about it, the issue tracker is open.