Bambda
Match flows with a line of Lua when the field query runs out — filter History, gate Intruder payloads, and drive rule and Workflow conditions, all in Community.
When the field query can't say what you mean, write the match as code. Bambda
takes a line of Lua over a flow and runs it anywhere Hugin filters or matches
traffic.
It's Hugin's version of Burp's filter-by-Bambda, written in Lua rather than Java — a Burp Bambda won't paste in unchanged, but the idea carries over: when Search and the filter fields fall short, drop to code. Bambda is part of Hugin's filtering, so it's Community with no Pro gate.
Bambda comes in two match forms. A full Lua expression over a flow table cuts
down History and drives the MCP tool. A one-line
predicate (req.host == "target.com") matches flows inside rules, Intruder, and
Workflows. A third, related form rewrites a flow on every send.
Filter flows with Lua
Write a Lua expression that returns true to keep a flow, false to drop it. It runs in three places:
Prefix a query with bambda: and the rest of the line is Lua, run over every
flow in the list. This is a whole-query mode — it replaces the field filter for
that query rather than combining with it.
A Filters capture rule can use a Bambda as its condition, then keep, flag, or drop the flow at the proxy. Flagging a match is how you pre-highlight a class of traffic before you reach History.
Drive it from an AI agent over MCP. The tool filters up
to 1,000 flows a pass, runs over full flows (headers and bodies, not just
metadata), and can also transform — return a value per flow to pull every token
or Set-Cookie out of History at once.
The flow table
In the History filter bar you get a flow's metadata. Headers and bodies aren't loaded there — they're empty strings and tables — so match on those through the MCP tool, which loads the full flow.
Available everywhere:
flow.method,flow.host,flow.path,flow.status(a number;0until the response lands)flow.content_type,flow.content_length(response size in bytes)flow.is_tls,flow.has_params,flow.latency_ms,flow.flagged,flow.tags(a list)
Full flows only (the MCP tool):
flow.url,flow.annotations(your notes on the flow)flow.request_headers["name"],flow.response_headers["name"]— names are lowercase,nilwhen the header is absentflow.request_body,flow.response_body(strings; opt in to bodies)
Examples
In the History filter bar, server errors with a body over 100 KB, then the unusual status codes worth a look:
bambda:return flow.status >= 500 and flow.content_length > 102400
bambda:return flow.status ~= 200 and flow.status ~= 301 and flow.status ~= 302 and flow.status ~= 304
Headers and bodies need full flows — run these through the MCP bambda tool. A
POST with a JSON request body that came back 200:
local ct = flow.request_headers["content-type"] or ""
return flow.method == "POST" and ct:find("json") ~= nil and flow.status == 200
Flows to an API host carrying an Authorization header:
return flow.host:match("api%.") ~= nil
and flow.request_headers["authorization"] ~= nil
Responses that set a cookie without HttpOnly — session cookies in reach of
script:
local sc = flow.response_headers["set-cookie"]
return sc ~= nil and sc:lower():find("httponly") == nil
The editor ships presets for the common hunts — server errors, requests with
parameters, Authorization present, Set-Cookie responses, no-cache responses,
empty bodies, interesting status codes — and a Generate from prompt button
that writes the Lua from a description. Save your own as a named preset to reuse
across targets.
The Lua runs sandboxed: no os, io, require, debug, or file access, and a
100,000-instruction cap per flow stops a runaway loop. A script error drops the
offending flow, not the whole filter.
Match flows with a predicate
The predicate is a one-line boolean over a flow, no return, no full Lua. It
reads:
req.host req.path req.method req.url req.header(NAME) req.body
resp.status resp.header(NAME) resp.body
with the operators ==, !=, contains, starts_with, ends_with, and
matches (regex), joined by && and ||. Header names go bare in the
parentheses and match case-insensitively; the value on the right is quoted.
resp.* only resolves once the response is in — on the request side those
clauses are false.
It's the condition language for matching flows beyond a field comparison:
Use a Bambda as a rule condition so an edit fires only on the flows you mean.
Gate each payload through a predicate (the payload lands in req.body), so
Intruder skips the requests you don't want to send.
Branch a Workflow on whether the current flow matches.
Drive an access-control check off a flow condition.
Some predicates:
resp.status == 403
req.method == "POST" && req.header(Content-Type) contains "json"
req.header(Authorization) starts_with "Bearer"
req.body matches "\d{4,}"
resp.body contains "SQL syntax"
A predicate that doesn't parse matches nothing — it fails shut rather than firing
on every flow. Regex in matches runs in linear time with no catastrophic
backtracking, and a pattern is capped at 4 KiB, so a bad expression can't hang
the proxy.
The predicate has no >=. For a status class, write resp.status starts_with "5" or resp.status matches "^5..", not resp.status >= 500. Numeric
comparison like flow.status >= 500 belongs to the Lua flow filter above, which
is a different language.
Rewrite a flow on every send
The third form mutates traffic instead of matching it. Attach a Bambda to Repeater — an outbound script before the request goes out, an inbound script after the response returns — or use it as the Bambda Script edit in Match and replace.
The script gets the request as globals (method, url, host, path,
headers, body) and the response (status, headers, body); reassign any
of them to change the flow. A read-only env table exposes your
Environment variables, so env.TOKEN weaves a
saved value into a header on every send. Same sandbox, same 100,000-instruction
cap.
Reach for the Lua flow filter when you're cutting History down to the flows you want. Reach for the predicate when a rule, payload, or Workflow step needs a yes/no on the current flow. Reach for a Repeater Bambda when you need to change a request every time you fire it — sign a body, refresh a nonce, inject a token.