Why Vex

Most security automation is glue: shell out to a tool, scrape its text, guess at the fields, paste it into the next command. Vex replaces that with a small typed language where tools are functions, results are structured, and mistakes surface before anything runs.

Tools are typed functions

You call a tool and get back a structured record, not a blob of text to re-parse.

input domain: host

subs = subfinder(domain)
live = httpx(subs.subdomains)
out(head(live.urls, 5))

Field access is checked. A typo like live.rul is a compile error with a suggestion, not a silent undefined three steps later. You find out at check time, not mid-scan.

Strings that can’t become an injection

Interpolation quotes values for you. There is no way for an interpolated value to break out and become part of the command.

input name: str
endpoint = `https://${name}.example.test/health`
print(endpoint)

The escape hatch is deliberate and visible: an intentionally unquoted fragment must be wrapped in raw(...), so the one risky line is the one you can see. Dynamic pre-built command strings are simply rejected.

Concurrency with a ceiling

Loops are easy to parallelize and hard to run away.

input endpoints: list<url>

parallel(max=8) for endpoint in endpoints {
    response = http.get(endpoint, timeout=3s)
    out(response.status)
}

repeat refuses to run without a bound, so a retry loop can’t spin forever:

attempts = 0
repeat(max=5, every=1s) until attempts >= 3 {
    attempts = attempts + 1
    print(attempts)
}

Inputs and secrets are declared

Run parameters are typed and validated at the start. Credentials are named, never pasted into source, and masked wherever they render, while still working in the requests that need them.

input target: url

response = http.get(target, timeout=5s)
out(response.status)

Findings are a result, not a side effect

Checks and findings are part of the language. expect records a check; add a severity and it files a finding. The run keeps going either way.

input endpoint: url

response = http.get(endpoint, timeout=5s)
expect response.status == 200, "Endpoint did not return 200", severity="medium"
out(response.status)

You only carry what you surface

A program can hold thousands of rows in the sandbox; only what you pass to out() or show() leaves it. You pay for the five results you asked to see, not the ten thousand you scanned.

Checked before it runs

Every program is parsed, type-checked, and validated against the tool registry before execution. If it won’t run cleanly, it doesn’t run. The check is the safety boundary, and it runs inside a sandbox with a bounded budget.

Keep going

  • The language: syntax, types, control flow, and findings.
  • Look up available tools and their parameters in your environment before you call them, and only test targets you are authorized to assess.

Improve team velocity with
better security and privacy.