Table of Contents
- Overview
- Setup
- Extension Types
- Writing a JavaScript Extension
- Writing a YAML Extension
- Quick Checks
- Snippets
- Context Objects Reference
- API Reference
- Testing Your Extension
- Configuration Reference
- Tips and Best Practices
Overview
Extensions plug into the scanner pipeline at four points:| Type | Runs when | Use for |
|---|---|---|
active | During audit phase | Send payloads, detect vulnerabilities |
passive | Analyzing captured traffic | Inspect request/response without new traffic |
pre_hook | Before each request is sent | Modify requests, skip assets, inject headers |
post_hook | After a finding is emitted | Escalate severity, drop false positives |
Setup
1. Enable extensions in your config
Add or uncomment theextensions block under audit in your vigolium-configs.yaml:
2. Place your extension file
Drop any.js or .vgm.yaml file into your extension_dir. Vigolium discovers them automatically on the next scan.
3. Verify it loaded
Extension Types
Module export contract (JS)
Every JS extension must export amodule.exports object. Required fields:
| Field | Required | Description |
|---|---|---|
id | No (auto from filename) | Unique identifier |
type | Yes | active, passive, pre_hook, post_hook |
name | No | Display name |
description | No | What the extension does |
severity | For active/passive | critical, high, medium, low, info, suspect |
confidence | No | tentative, firm, certain |
scanTypes | For active/passive | ["per_insertion_point"], ["per_request"], ["per_host"] |
tags | No | Classification tags for --module-tag filtering (e.g. ["custom", "xss"]) |
Writing a JavaScript Extension
JS extensions run inside an embedded Sobek (ES5.1-compatible) VM. The globalvigolium object provides all APIs.
Active Module (JS)
Active modules send modified requests to probe for vulnerabilities. Declare which scan granularity you need inscanTypes:
per_insertion_point— called once per parameter (query, body, header, cookie)per_request— called once per request/response pairper_host— called once per unique hostname
null if nothing found.
Each finding object:
Passive Module (JS)
Passive modules analyze existing request/response pairs without making new requests. Add ascope field to limit to "request", "response", or "both" (default).
Pre-Hook (JS)
Pre-hooks run before each request is sent to a module. Return the modified request, a headers-only patch, ornull to skip the request entirely.
| Return | Effect |
|---|---|
request (unchanged) | Pass through as-is |
{ headers: {...} } | Merge these headers into the request |
{ raw: "GET /..." } | Replace the entire raw request |
null | Skip this request (module won’t see it) |
Post-Hook (JS)
Post-hooks receive each emitted finding. Return the (possibly modified) result, ornull to suppress the finding.
Writing a YAML Extension
YAML extensions (.vgm.yaml) are a declarative alternative for common patterns. They require no programming knowledge and are compiled to the same internal module interface as JS extensions.
Active Module (YAML)
Userules to define match-then-emit pairs. Each rule specifies a match condition and the finding to emit when it matches.
| Field | Description |
|---|---|
tags | Classification tags for --module-tag filtering |
scan_types | per_insertion_point, per_request, per_host |
payloads | List of strings to inject (used with per_insertion_point) |
matchers | List of MatcherDef — all applied to the same finding |
matchers_condition | or (default) or and — how matchers combine |
finding | Single finding emitted when matchers pass |
rules | List of {match, finding} pairs — evaluated independently |
rules when different patterns should emit different findings. Use matchers + finding when all conditions must be true together.
Matcher types:
Passive Module (YAML)
Passive YAML modules use the samerules structure but do not send new requests:
rules[].match:
| Field | Description |
|---|---|
body_contains | Response body contains string |
body_regex | Response body matches regex |
response_header | Response header name exists |
regex | Additional regex to apply to the header value |
contains | String the header value must contain |
status | List of HTTP status codes |
Pre-Hook (YAML)
Pre-hooks in YAML support header injection, extension skipping, and conditional skipping. Inject headers:| Field | Description |
|---|---|
add_headers | Map of header name → value to inject |
skip_extensions | URL path suffixes that cause the request to be skipped |
skip_when.config_empty | Skip if this config variable is empty |
skip_when.url_contains | Skip if URL contains any of these strings |
| Template | Expands to |
|---|---|
{{config.VAR_NAME}} | User-defined config variable |
{{rand(N)}} | Random alphanumeric string of length N |
Post-Hook (YAML)
Post-hooks in YAML can escalate severity or drop findings based on URL patterns. Escalate severity for critical paths:Quick Checks
Quick checks are the lightest extension format — declarative JSON objects that define “send payload, check response” patterns with zero JavaScript. They’re ideal for agent-generated checks and rapid iteration.Per Insertion Point
Inject payloads into each parameter and check the response:Per Request / Per Host
Send specific requests and check responses:Match Fields
Match conditions use OR logic:| Field | Description |
|---|---|
body_contains | Response body contains string |
body_regex | Response body matches regex |
status | HTTP status code equals value |
header_contains | Response header contains string |
Rules
idmust be lowercase with hyphens (e.g."ssti-jinja2")scanis one of:per_insertion_point,per_request,per_hostseverityis one of:critical,high,medium,low,info- Quick checks are automatically wrapped into full extension modules at runtime
Snippets
Snippets are a middle ground between quick checks and full extensions — you write just the function body (no boilerplate), and it gets wrapped in a module scaffold automatically. Use snippets when you need custom logic orvigolium.* API access.
Format
Available Variables
Inside the snippet body, you have access to:| Variable | Description |
|---|---|
ctx | Request/response context (ctx.request, ctx.response, ctx.record) |
insertion | Insertion point object (only for per_insertion_point scan type) |
vigolium.http | HTTP requests, sessions, batch, replay, sequence, auth testing |
vigolium.db | Database queries for records and findings |
vigolium.utils | Encoding, hashing, diff, JWT, CSS selectors, etc. |
vigolium.parse | URL, request, response, HTML parsing |
vigolium.scan | Module listing, scope, finding creation |
vigolium.source | Source code access and search |
vigolium.agent | AI-augmented analysis |
vigolium.oast | Out-of-band testing |
vigolium.ingest | Data ingestion |
vigolium.payloads() | Built-in payload wordlists |
Rules
idmust be lowercase with hyphensscanis one of:per_insertion_point,per_request,per_hostbodycontains the function body as a string (newlines escaped as\n)- The return value follows the same convention as full extensions: array of findings or
null
Context Objects Reference
ctx — passed to all active/passive module functions
insertion — second arg to scanPerInsertionPoint
ctx.record — current HTTP record context
API Reference
vigolium.log
| Function | Description |
|---|---|
info(msg) | Log info message |
warn(msg) | Log warning message |
error(msg) | Log error message |
debug(msg) | Log debug message |
vigolium.utils
Encoding:| Function | Description |
|---|---|
base64Encode(s) / base64Decode(s) | Base64 encode/decode |
urlEncode(s) / urlDecode(s) | URL encode/decode |
htmlEncode(s) / htmlDecode(s) | HTML entity encode/decode |
| Function | Description |
|---|---|
sha1(s) | SHA-1 hash |
sha256(s) | SHA-256 hash |
md5(s) | MD5 hash |
| Function | Description |
|---|---|
randomString(len) | Random alphanumeric string |
| Function | Description |
|---|---|
regexMatch(str, pattern) | Test if string matches regex |
regexExtract(str, pattern) | Extract matches from string |
| Function | Description |
|---|---|
readFile(path) | Read file contents |
readLines(path) | Read file as array of lines |
writeFile(path, data) | Write data to file |
mkdir(path) | Create directory |
glob(pattern) | Find files matching pattern |
| Function | Description |
|---|---|
parse_url(url, format) | Parse URL with format |
pathToTemplate(path) | Replace dynamic segments with * |
hasDynamicSegment(path) | Check for dynamic segments (IDs, UUIDs) |
| Function | Description |
|---|---|
toSet(csv) | Convert CSV string to {key: true} map |
extractParamNames(str) | Extract deduplicated param names from query/body |
| Function | Description |
|---|---|
diff(a, b) | Line-by-line comparison → {added, removed, similarity} |
similarity(a, b) | Jaccard similarity (0.0-1.0) on word tokens |
diffResponses(a, b) | Structural HTTP response comparison → {status_match, body_similarity, header_diff, body_diff, length_diff, likely_same_content} |
| Function | Description |
|---|---|
cssSelect(html, selector) | CSS selector query → [{text, attrs, html}] |
| Function | Description |
|---|---|
extractToken(response, rules) | Extract tokens from HTTP response using configurable rules (json/header/cookie/regex) |
| Function | Description |
|---|---|
jwtDecode(token) | Decode JWT without verification → {header, payload, signature} |
jwtEncode(payload, opts?) | Forge JWT (HS256/HS384/HS512/none) |
jwtExpired(token) | Check if JWT is expired |
| Function | Description |
|---|---|
multipart(fields) | Build multipart/form-data body → {body, contentType} |
| Function | Description |
|---|---|
detectAnomaly(responses) | Score responses by divergence → [{index, score}] |
| Function | Description |
|---|---|
sleep(ms) | Sleep for milliseconds |
exec(cmd) | Execute shell command (requires allow_exec) → {stdout, stderr, exitCode} |
getEnv(name) / setEnv(name, value) | Environment variables |
jsonExtract(json, path) | Extract value from JSON by path |
vigolium.parse
| Function | Description |
|---|---|
url(str) | Parse URL → {scheme, host, hostname, port, path, query, fragment, params, segments, template} |
request(raw) | Parse raw HTTP request → {method, path, query, version, headers, body, host, params, cookies} |
response(raw) | Parse raw HTTP response → {status, statusText, version, headers, body, cookies, contentType} |
headers(str) | Parse header block → {name: value} |
cookies(str) | Parse Cookie header → {name: value} |
query(str) | Parse query string → {name: value} |
json(str) | Parse JSON string → native value |
form(body) | Parse URL-encoded form → {name: value} |
html(str) | Parse HTML → {forms, links, scripts, meta} |
vigolium.http
Basic requests:| Function | Description |
|---|---|
get(url, opts?) | HTTP GET |
post(url, body, opts?) | HTTP POST |
request(opts) | Full control (method, url, headers, body) |
send(rawRequest) | Send raw HTTP request string |
buildRequest(rawRequest, overrides) | Clone and modify a raw request (method, path, headers, body, query) |
| Function | Description |
|---|---|
session(opts?) | Create persistent session with shared cookie jar and default headers |
login(opts) | Send credentials, extract auth tokens, return authenticated session |
sessionPool(configs) | Create named session pool from config map |
followAuth(opts) | Execute OAuth2 flow (client_credentials, password, code grants) |
get(), post(), request(), send(), setHeader(), removeHeader(), getHeaders(), getCookies(), setCookie(), cloneAs(), onRequest(), onResponse(), setAutoRefresh().
Batch and replay:
| Function | Description |
|---|---|
batch(requests, opts?) | Send multiple requests in parallel (configurable concurrency) |
replay(rawRequest, variations) | Replay request with multiple variations (header overrides, body swaps) |
| Function | Description |
|---|---|
sequence(steps) | Execute request sequence with variable extraction ({{varName}}), conditional execution, fallback steps, and repeat loops |
| Function | Description |
|---|---|
authTest(opts) | Test IDOR/BOLA by replaying requests across sessions with different privilege levels |
csrf(url, opts?) | Extract CSRF token from page (form, meta, header, cookie sources) |
| Function | Description |
|---|---|
retry(request, opts?) | Retry with configurable backoff (max retries, retry_on status codes, until predicate) |
cache(opts?) | Enable response caching with TTL and max entries |
clearCache() | Clear all cached responses |
cachedGet(url, opts?) | GET with cache |
cachedRequest(opts) | Full request with cache |
| Function | Description |
|---|---|
graphql(url, opts) | Send GraphQL query/mutation → {data, errors, raw} |
graphqlSchema(url, opts?) | Fetch introspection schema |
vigolium.scan
| Function | Description |
|---|---|
listModules() | List all registered modules |
listModuleTags() | List all unique module tags |
listModulesByTag(tag) | List modules with a specific tag |
isInScope(host, path) | Check if host/path is in scope |
getScope() / setScope(scope) | Get/set scope configuration |
createFinding(finding) | Persist a finding |
getCurrentScan() | Get current scan info |
startNewScan(opts) | Start a new scan |
scanRecords(opts) | Queue scan for existing records by UUIDs |
vigolium.ingest
| Function | Description |
|---|---|
url(url) | Ingest a single URL |
urls(content) | Ingest multiple URLs from content |
curl(command) | Ingest from curl command |
raw(rawRequest, rawResponse?) | Ingest raw HTTP request/response |
openapi(spec, opts?) | Ingest from OpenAPI spec |
postman(collection) | Ingest from Postman collection |
vigolium.source
| Function | Description |
|---|---|
list(hostname?) | List source repos |
get(id) | Get source repo by ID |
getByHostname(hostname) | Get repos by hostname |
readFile(hostname, path) | Read source file |
listFiles(hostname, glob?) | List source files |
searchFiles(hostname, pattern) | Search source files by pattern |
vigolium.agent (AI-augmented)
| Function | Description |
|---|---|
complete(opts) | Full control: model, messages, schema, temperature → {content, model, tokens_in, tokens_out} |
ask(prompt, opts?) | Single prompt → text response |
chat(messages, opts?) | Conversation → text response |
generatePayloads(opts) | Generate context-aware security payloads by type, context, technology, WAF |
analyzeResponse(opts) | Analyze HTTP exchange for vulnerability → {vulnerable, confidence, evidence, details} |
confirmFinding(opts) | Verify true positive → {confirmed, confidence, reasoning, false_positive_indicators} |
run(opts) | Run full agent backend subprocess (claude, opencode, gemini, etc.) |
vigolium.oast (Out-of-Band Testing)
| Function | Description |
|---|---|
enabled() | Check if OAST service is active |
payload(targetURL?, paramName?, injectionType?) | Generate unique OAST callback URL → {url} |
poll(timeoutMs?) | Wait then return all OAST interactions → [{protocol, unique_id, remote_address, target_url, parameter_name, module_id, interacted_at}] |
vigolium.db
Records:| Function | Description |
|---|---|
records.query(filters?) | Query HTTP records with filters (hostname, path, methods, status_codes, source, search, fuzzy, min_risk_score, limit, offset, sort) |
records.get(uuid) | Get single record by UUID |
records.getRelated(uuid, opts?) | Get records with same path template/hostname |
records.annotate(uuid, patch) | Update risk_score/remarks |
records.grouped(opts?) | Group by path template for IDOR detection → [{template, method, records, param_values}] |
| Function | Description |
|---|---|
findings.query(filters?) | Query findings (severity, module_name, scan_uuid filters) |
findings.get(id) | Get finding by ID |
findings.getByRecord(uuid) | Get findings for an HTTP record |
findings.create(finding) | Persist a new finding |
| Function | Description |
|---|---|
compareResponses(records) | Anomaly detection across record set → {all_similar, scores, variant_count, summary} |
vigolium.payloads(type)
Returns built-in payload wordlists by vulnerability type. Types:"xss", "sqli", "ssti", "ssrf", "lfi", "path_traversal", "xxe", "cmdi", "open_redirect", "crlf".
vigolium.config
Read-only config values from thevariables block in vigolium-configs.yaml:
Testing Your Extension
Run only the extension phase
The fastest way to test your extension against already-ingested traffic without running a full scan:Test against a live target
To ingest fresh traffic and immediately run only extensions:Use a one-off config with a custom extension path
You don’t need to copy files to~/.vigolium/extensions/. Use custom_dir to point directly at your file:
my-test-config.yaml:
Verify your extension loads
Before running a scan, check your extension is discovered and parsed correctly:Browse the built-in API reference
Install preset examples to learn from
Configuration Reference
Fullextensions block options in vigolium-configs.yaml:
Tips and Best Practices
Returnnull, not [] — returning an empty array is treated the same as null, but null is the conventional no-finding signal.
Check for nil before accessing properties:
vigolium.utils.randomString for canaries to avoid collisions between concurrent extension invocations.
Keep pre-hooks fast — they run on every request before any module sees it. Avoid HTTP calls inside pre-hooks.
YAML vs JS vs quick check decision guide:
- Use quick checks when you need simple payload-and-match patterns with no logic
- Use snippets when you need
vigolium.*API access but don’t want full boilerplate - Use YAML when you need regex/header/status matching with a fixed finding output
- Use JS when you need: conditional logic, multiple HTTP requests, encoding/decoding, database lookups, session management, or AI-augmented analysis
scope: "response" if you only need response data. This avoids unnecessary invocations.
Use vigolium.config.* for secrets and environment-specific values instead of hardcoding them:
--only extension and a small known dataset so your module’s console.log output is easy to read.