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.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add vigolium/vigolium --skill write-jsext --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Write Jsext?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/vigolium-write-jsext)More formats (shields.io, HTML) on the badges page.
---
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:
1. Hand the file path or inline source to **`run_extension`** to execute it
immediately and get back findings.
2. 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)
```javascript
module.exports = {
id: "kebab-case-id", // unique within the run
name: "Human-readable name",
description: "1–2 sentence purpose",
type: "passive", // required
severity: "low", // critical|high|medium|low|info
confidence: "tentative", // certain|firm|tentative
scope: "response", // request|response|both — what to read
tags: ["exposure", "light"], // free-form classification
scanTypes: ["per_request"], // only "per_request" for passive
scanPerRequest: function(ctx) {
// ctx.request: { url, method, headers, body, ... }
// ctx.response: { status, headers, body, ... }
// ctx.record: { uuid, addRemarks(tags) } (DB-backed records only)
//
// Return null when there's nothing to report, OR an array of finding
// objects (see "Finding shape" below). Throwing is fine — vigolium
// catches per-record errors and continues.
}
};
```
### Active (sends new requests; can mutate inputs and probe)
```javascript
module.exports = {
id: "active-extension-id",
name: "Active Extension",
description: "What it tests for",
type: "active", // required
severity: "medium",
confidence: "tentative",
tags: ["injection", "custom"],
scanTypes: ["per_request"], // most common; per_host / per_insertion_point also exist
scanPerRequest: function(ctx) {
// Same ctx as passive, plus you can call vigolium.http.* to send
// new requests, and vigolium.parse.* / vigolium.utils.* for helpers.
// Return null or an array of findings.
}
};
```
### Finding shape (what scanPerRequest returns)
```javascript
return [{
url: ctx.request ? ctx.request.url : "", // required
matched: "the substring or evidence", // shown verbatim in reports
name: "Specific bug title", // overrides module name
description: "Markdown-friendly evidence + reasoning",
severity: "high", // overrides module severity
// optional:
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`
```javascript
vigolium.http.get(url, opts) // -> HttpResponse
vigolium.http.post(url, body, opts) // -> HttpResponse
vigolium.http.request({ url, method, headers, body })
vigolium.http.send(rawHttpRequestString) // raw request, ignores cookies
vigolium.http.batch([req1, req2, ...]) // parallel; returns []
vigolium.http.session({ headers, cookies }) // persistent jar + defaults
vigolium.http.login({ url, body, ... }) // returns authed session
vigolium.http.cachedGet(url, opts) // memoized within the run
```
`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`
```javascript
vigolium.parse.url(url) // { scheme, hostname, port, path, query, ... }
vigolium.utils.hasDynamicSegment(path) // true for /users/123, /items/<uuid>
vigolium.utils.pathToTemplate(path) // /users/123 -> /users/*
vigolium.utils.regexMatch(haystack, regex) // boolean
vigolium.utils.regexExtract(haystack, regex) // array of matches (capture group 1)
vigolium.utils.md5(s) / sha256(s) / base64Encode(s)
```
### `vigolium.log`
```javascript
vigolium.log.info("...") // visible in runtime.log + verbose output
vigolium.log.warn("...") // surfaces in operator console
vigolium.log.error("...") // tagged red; doesn't abort the script
```
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)
```javascript
vigolium.db.records.query({ hostname, path, method, limit })
vigolium.db.records.annotate(uuid, { risk_score, remarks })
vigolium.db.compareResponses(records) // anomaly grouping helper
vigolium.db.findings.query({ scanUUID, severity })
```
### `vigolium.scan`
```javascript
vigolium.scan.isInScope(host, path) // honor scope rules in custom checks
vigolium.scan.getCurrentScan() // { uuid: "<scan-uuid>" }
vigolium.scan.createFinding(findingObj) // bypasses return-array path
```
### `vigolium.oast` (out-of-band callback testing)
```javascript
var oast = vigolium.oast.allocate(); // { url, hostname, ... }
// fire a request that triggers a callback to oast.url
var hits = vigolium.oast.poll(oast.id, 30); // wait up to 30s for callbacks
```
Use this for SSRF, blind XSS, blind SQLi, log4shell-style probes.
### `vigolium.agent` (LLM-assisted analysis — optional, may be unavailable)
```javascript
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
1. **No `import` / `export` / `require`** beyond `module.exports`. Stick to
plain JS plus the `vigolium.*` globals.
2. **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, { ... })`.
3. **Regex flags use string syntax**, not literal `/.../i`. The engine
accepts a string pattern; pass `"(?i)foo"` for case-insensitive.
4. **Skip irrelevant content types.** Most passive checks should bail
early on CSS / images / fonts — see `internal_url_leak.js` for the
pattern.
5. **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).
6. **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)
```javascript
// detect-debug-headers.js
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:
1. **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.
2. **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.
3. **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.
4. **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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!