| name | write-jsext |
| description | Reference for writing custom vigolium JavaScript extensions. Use when you need to author a one-off scanner module — passive (reads existing HTTP records) or active (sends new requests) — and run it via the run_extension tool. Covers the module shape, the vigolium.* API surface, and the common pitfalls. |
| license | MIT |
| allowed-tools | ["read_file","write_file","edit_file","run_extension","run_scan"] |
Writing a Custom Vigolium JS Extension
You're authoring a single-file JavaScript extension that the vigolium scanner
will load and run. Two output paths from here:
- Hand the file path or inline source to
run_extension to execute it
immediately and get back findings.
- Write the file to disk (e.g. into the session dir) for the operator to
reuse later via
vigolium scan --ext path/to/script.js.
You do not need a build step. Vigolium's embedded JS engine (Sobek) runs
the script directly. ES2017 features work; no import / export — use
module.exports.
Module Shape
Every extension exports one object via module.exports. The shape decides
whether it runs as a passive or active scanner module.
Passive (read-only, runs against stored HTTP records)
module.exports = {
id: "kebab-case-id",
name: "Human-readable name",
description: "1–2 sentence purpose",
type: "passive",
severity: "low",
confidence: "tentative",
scope: "response",
tags: ["exposure", "light"],
scanTypes: ["per_request"],
scanPerRequest: function(ctx) {
}
};
Active (sends new requests; can mutate inputs and probe)
module.exports = {
id: "active-extension-id",
name: "Active Extension",
description: "What it tests for",
type: "active",
severity: "medium",
confidence: "tentative",
tags: ["injection", "custom"],
scanTypes: ["per_request"],
scanPerRequest: function(ctx) {
}
};
Finding shape (what scanPerRequest returns)
return [{
url: ctx.request ? ctx.request.url : "",
matched: "the substring or evidence",
name: "Specific bug title",
description: "Markdown-friendly evidence + reasoning",
severity: "high",
request: ctx.request ? ctx.request.raw : "",
response: ctx.response ? ctx.response.raw : "",
tags: ["custom", "graphql"]
}];
Or, instead of returning, you can call vigolium.scan.createFinding(obj)
directly. Returning is preferred — it composes better with the executor's
deduplication and rate-limiting.
Core API Cheat-Sheet
Only the most common pieces. Full TypeScript defs live at
pkg/jsext/vigolium.d.ts — read it if you need an obscure helper.
vigolium.http
vigolium.http.get(url, opts)
vigolium.http.post(url, body, opts)
vigolium.http.request({ url, method, headers, body })
vigolium.http.send(rawHttpRequestString)
vigolium.http.batch([req1, req2, ...])
vigolium.http.session({ headers, cookies })
vigolium.http.login({ url, body, ... })
vigolium.http.cachedGet(url, opts)
HttpResponse exposes status, headers, body, raw, plus helpers like
.json(), .text(). Treat headers as case-insensitive but values as
arrays-or-strings; normalize with vigolium.utils.headerValue(h, "name").
vigolium.parse and vigolium.utils
vigolium.parse.url(url)
vigolium.utils.hasDynamicSegment(path)
vigolium.utils.pathToTemplate(path)
vigolium.utils.regexMatch(haystack, regex)
vigolium.utils.regexExtract(haystack, regex)
vigolium.utils.md5(s) / sha256(s) / base64Encode(s)
vigolium.log
vigolium.log.info("...")
vigolium.log.warn("...")
vigolium.log.error("...")
Don't use console.log — it's a no-op in this engine.
vigolium.db (only when a repository is wired up — usually true at run time)
vigolium.db.records.query({ hostname, path, method, limit })
vigolium.db.records.annotate(uuid, { risk_score, remarks })
vigolium.db.compareResponses(records)
vigolium.db.findings.query({ scanUUID, severity })
vigolium.scan
vigolium.scan.isInScope(host, path)
vigolium.scan.getCurrentScan()
vigolium.scan.createFinding(findingObj)
vigolium.oast (out-of-band callback testing)
var oast = vigolium.oast.allocate();
var hits = vigolium.oast.poll(oast.id, 30);
Use this for SSRF, blind XSS, blind SQLi, log4shell-style probes.
vigolium.agent (LLM-assisted analysis — optional, may be unavailable)
var verdict = vigolium.agent.confirmFinding({
name: "Reflected XSS",
request: ctx.request.raw,
response: ctx.response.raw,
matched: payload
});
if (verdict && verdict.confirmed) { }
vigolium.agent.generatePayloads({ type: "xss", context: "html", count: 5 });
Always null-check the result — vigolium.agent.* returns null when no LLM
client is configured.
Common Pitfalls
- No
import / export / require beyond module.exports. Stick to
plain JS plus the vigolium.* globals.
- Don't mutate
ctx.request / ctx.response — they're shared with
other modules. If you need a modified request, build a new one with
vigolium.http.buildRequest(ctx.request.raw, { ... }).
- Regex flags use string syntax, not literal
/.../i. The engine
accepts a string pattern; pass "(?i)foo" for case-insensitive.
- Skip irrelevant content types. Most passive checks should bail
early on CSS / images / fonts — see
internal_url_leak.js for the
pattern.
- Always set
confidence: "tentative" for heuristic checks. Save
firm / certain for cases where you've actually verified the bug
(e.g., OAST callback received, response diff confirmed).
- Per-request modules are fan-out. They run for every record in
scope — keep them cheap. Use
vigolium.http.cachedGet for repeated
lookups, and bail on the first signal that the check doesn't apply.
Minimal Working Example (passive)
module.exports = {
id: "debug-headers",
name: "Debug headers exposed",
description: "Flags responses leaking X-Debug-* / X-Powered-By in production-looking apps",
type: "passive",
severity: "low",
confidence: "firm",
scope: "response",
tags: ["exposure", "headers", "light"],
scanTypes: ["per_request"],
scanPerRequest: function(ctx) {
if (!ctx.response || !ctx.response.headers) return null;
var leaked = [];
var headers = ctx.response.headers;
for (var name in headers) {
var lower = name.toLowerCase();
if (lower.indexOf("x-debug") === 0 ||
lower === "x-powered-by" ||
lower === "server") {
leaked.push(name + ": " + headers[name]);
}
}
if (leaked.length === 0) return null;
return [{
url: ctx.request.url,
matched: leaked.join("; "),
name: "Debug / framework headers exposed",
description: "Response includes verbose framework headers:\n" +
leaked.map(function(l) { return "- `" + l + "`"; }).join("\n"),
severity: "low"
}];
}
};
Iteration Loop
Once you have a draft:
- Validate by running it. Call
run_extension with script_source
(or script_path if you wrote it to disk first). Pass concrete
targets so you get real findings back, not just a smoke test.
- Read the result struct.
finding_count > 0 is your signal that
the matcher fires. Zero usually means a regex / scope mistake — add
vigolium.log.info(...) lines and re-run.
- Tighten before declaring success. False positives are the
default — a passing run on one URL doesn't mean the rule is good.
Run against at least 2–3 targets, including one that should NOT
match.
- Persist when settled. If the operator wants this to run as part
of regular scans, write the file under
<sessionDir>/extensions/
or a project-level extensions directory.
When NOT to Write an Extension
If the bug class already has a built-in module (xss, sqli, ssrf, idor,
etc.), prefer run_scan with modules: ["<id>"] over a hand-written
extension. Extensions are for novel logic that doesn't fit the
generic scanner shape — protocol quirks, app-specific invariants,
correlation across records, custom OAST flows.