Skip to main content
availability

Deployment: Invicti Platform on-demand, Invicti Platform on-premises

Custom vulnerability checks

If you need to validate highly specific response conditions, detect undisclosed or cutting-edge vulnerabilities, or enforce your organization's internal security standards, you can write your own checks in JavaScript and have Invicti run them as part of every scan. Results appear in the UI and flow to connected issue trackers, exactly like built-in checks.

This document explains how to write and use custom vulnerability checks in Invicti Platform.

Why this matters

Built-in checks cover well-known vulnerabilities across standard frameworks. Custom checks let you go beyond that - to validate highly specific response conditions, detect cutting-edge or undisclosed vulnerabilities before they appear in the standard database, or enforce your organization's internal security standards. You define the security logic, Invicti runs it on every scan.

Upload a custom security check

Upload your script through the Invicti Platform UI to make it available for use in scan profiles.

  1. Select Scans > Custom security checks from the left-side menu.
  2. Click New security check.
  3. Set Category to Target or Location depending on what your check needs to do:
    • Target - runs once per scan. Use for one-time checks, such as testing whether a specific endpoint exists or probing how the server responds to a particular request.
    • Location - triggered when a new endpoint or path is discovered during the scan. Use when your check needs to examine each discovered location - it has access to the response body, headers, and the request that triggered it.
  4. Under File, upload your .js script file.
  5. Enter a Name for the check.
  6. Set the Severity level.
  7. Optionally add a Title and Description.
  8. Save the check.
New security check form showing Category, File, Name, Severity, Title, and Description fieldsNew security check form showing Category, File, Name, Severity, Title, and Description fields

Add custom security checks to a scan profile

After uploading your checks, add them to a scan profile to control which scans run them.

  1. Select Scans > Custom profiles from the left-side menu.
  2. Click Add new profile.
  3. Enter a Name and optionally a Description for the profile.
  4. In Checks to include, type Custom in the filter to find your uploaded checks.
  5. Select the checks you want to include using the checkboxes.
  6. Click Save.

Once saved, select this scan profile when launching a scan.

Scan launch screen showing the custom scan profile selected under scanning optionsScan launch screen showing the custom scan profile selected under scanning options

Examples

Target check: detect an accessible admin console

This Target check probes the /admin/ endpoint and raises a vulnerability alert if the admin console is accessible.

// create a custom HTTP job
let job = ax.http.job();
// set its URL to the scan target URL
job.setUrl(scriptArg.target.url);
// probe the /admin/ endpoint
job.request.uri = '/admin/';
// execute the request against the target
ax.http.execute(job).sync();

if (!job.error
&& job.response.status !== 404
&& job.response.body.indexOf('Admin Console</title>') != -1) {

scanState.addVuln({
location: scriptArg.target.root, // affects the server, so attach to /
http: job, // the HTTP request/response will show up in the UI
vulnerability_type: {
name: 'Admin console is accessible on target',
severity: 'high',
description: 'The admin endpoint should be restricted for internal use.',
impact: 'An attacker may discover it and use it to exploit the application.',
recommendation: 'Restrict the admin panel to the corporate VPN.',
}
});
}

Location check: detect a compressed archive at each discovered directory

This Location check runs on each discovered directory and raises a vulnerability alert if an archive.zip file is accessible at that location.

if (scriptArg.location.url.endsWith('/')) // verify the new location is a directory
{
// create a custom HTTP job
let job = ax.http.job();
// set its URL to the location to check
job.setUrl(scriptArg.location.url + 'archive.zip');
// execute the request
ax.http.execute(job).sync();

if (!job.error
&& job.response.status == 200) {

scanState.addVuln({
location: scriptArg.location, // affects this location
http: job, // the HTTP request/response will show up in the UI
vulnerability_type: {
name: 'Compressed archive found on server',
severity: 'high',
description: 'A private compressed archive was discovered at the target URL.',
impact: 'An attacker may download the archive and use the information inside.',
recommendation: 'Remove the archive from the site.',
}
});
}
}

Script API reference

This section is for developers writing custom security check scripts. It covers the interfaces, methods, and properties available inside a .js file when authoring a check. If you're uploading a ready-made script, you can skip this section.

Use the scripting engine API to read scan context, send requests, report vulnerabilities, add unlisted endpoints, and log debug output. For the complete API definition, refer to the native.d.ts type definitions file included with the scripting engine.

Read scan context data

Show details

Use scriptArg to read information about the current scan context - the target, the location being tested, and the HTTP exchange that triggered your script:

PropertyDescription
scriptArg.targetInformation about the scan target. Properties: host (string), port (string), secure (boolean), ip (string array), url (string - full URL of the target), root (location object representing /). Example: scriptArg.target.url
scriptArg.locationIn Location checks: the path, name, and url of the location the script was invoked on. In Target checks: path returns '/' and name returns an empty string. Example: scriptArg.location.url
scriptArg.httpThe ax.http.Job object containing the HTTP request/response pair the script was invoked on. Examples: scriptArg.http.request.uri, scriptArg.http.response.body

Send custom HTTP requests

Show details

Use ax.http.Job to send a custom HTTP request from within your script - useful when you need to probe the target for a specific condition or resource that wasn't discovered automatically.

let job = ax.http.job();
job.hostname = 'www.example.com';
job.request.uri = '/';
ax.http.execute(job).sync();

Connection settings:

PropertyRequiredDefaultDescription
hostnameYes-Host or domain name to connect to. Example: www.example.com
secureNofalseWhether to use HTTPS/TLS.
portNo80 (443 if secure is true)TCP destination port as a string. Example: job.port = "8080"
timeoutNo30000Milliseconds before a pending connection is cancelled.
retriesNo3Number of times to retry a timed-out request.

Request settings (set on ax.http.Job.Request):

PropertyDescription
uriURI to request. Default: /
methodHTTP method. Default: GET
bodyRequest body
addHeader(name, value)Add a custom request header. Example: job.request.addHeader('X-Header', 'Value')
setUrl(url)Set the full URL for the request in one call instead of setting hostname, port, secure, and uri separately. Example: job.setUrl(scriptArg.target.url)

Before processing the response, check that the connection succeeded:

if (!job.error) {
// Proceed with response processing
}

Read the server response

Show details

After sending a request, read what the server returned through ax.http.Job.Response to determine whether a vulnerability condition exists:

PropertyDescription
versionHTTP version. Example: "HTTP/1.1"
statusNumeric HTTP status code. Example: 200
reasonString representation of the status code. Example: "OK"
headersResponse headers. Use has(name) to check for a header, get(name) to retrieve its value, and toString() for a plain text representation of all headers. Header names aren't case-sensitive.
bodyResponse body

Example:

if (job.response.headers.has('Content-Type')) {
ax.log(ax.LogLevelInfo, job.response.headers.get('content-type'));
}

Report a vulnerability from your script

Show details

Use scanState.addVuln() to raise a vulnerability alert. Invicti treats it exactly like a built-in check result: it appears in the UI and can be sent to connected issue trackers.

Custom checks must fully describe the vulnerability inline using a vulnerability_type object. Grouping and deduplication is done by content hash, so the full definition matters.

scanState.addVuln({
location: scriptArg.location,
vulnerability_type: {
name: 'Vulnerability name',
severity: 'high',
description: 'Description of the vulnerability.'
}
});

Properties:

PropertyRequiredDescription
locationYes (if path not set)An ax.state.Location object for where the vulnerability was found. Pass scriptArg.location to use the current invocation's location.
pathYes (if location not set)A string path to the affected location. Must be absolute (starting with /) and within the scope of the current scan. If Invicti isn't aware of the path, it assigns the vulnerability to the root / location instead.
vulnerability_typeYesObject describing the vulnerability. Must include name (string) and severity ('info', 'low', 'medium', 'high', or 'critical'). Optionally include description, impact, and recommendation (all strings).
httpNoThe HTTP request/response pair that triggered the alert. Pass scriptArg.http for the current invocation's pair, or a manually created ax.http.Job object. Displayed alongside the alert in the UI.
Deduplication

Invicti deduplicates vulnerability alerts using a content hash of name and severity. Two scripts can report the same vulnerability as long as both fields match. If duplicates exist, the last reported instance wins on all remaining fields - so keep description, http, and other fields consistent across scripts that report the same vulnerability.

Add unlisted endpoints to the scan

Show details

If a target has pages or endpoints that aren't referenced anywhere on the site, use scanState.hintLinks() to tell Invicti about them. Invicti then includes them in Discovery, Analysis, and Testing - so they get tested just like any other discovered page.

scanState.hintLinks(['/hidden-endpoint.php', '/admin/config']);

URIs must be absolute (starting with /) and within the scope of the current scan. Out-of-scope URIs are ignored.

Write debug logs

Show details

Use ax.log() to write log entries while you develop and test your scripts:

ax.log(ax.LogLevelInfo, 'Something happened.');
  • level - Severity of the log entry. Accepted values: ax.LogLevelInfo (1), ax.LogLevelWarning (2), ax.LogLevelError (3).
  • data - The value to log, usually a string or number.

To access the log files, enable target debugging before launching the scan. On the target's settings page, go to the Advanced tab and enable Debug scans for this target. After the scan completes, the log file location appears on the Events tab.

Scan Events tab showing the location of custom script debug log filesScan Events tab showing the location of custom script debug log files

Encode and decode strings

Show details

Use these helper functions when you need to encode or decode string values while processing requests and responses:

HTML encoding:

ax.util.htmlEncode(input: string): string;
ax.util.htmlDecode(input: string): string;

Base64 encoding:

ax.util.base64Encode(input: string): string;
ax.util.base64Decode(input: string): string;

Example:

let testString = 'This is <b>bold</b> text';
let encoded = ax.util.htmlEncode(testString);
ax.log(ax.LogLevelInfo, `Encoded: ${encoded}`);
// Output: Encoded: This is &lt;b&gt;bold&lt;/b&gt; text

let decoded = ax.util.htmlDecode(encoded);
ax.log(ax.LogLevelInfo, `Decoded: ${decoded}`);
// Output: Decoded: This is <b>bold</b> text

Parse XML responses

Show details

Use ax.struct.parseXml() to parse XML responses. The function returns a DOMDocument object regardless of whether the input is valid XML - always check the error property before processing:

let parsedXML = ax.struct.parseXml(responseBody);

if (parsedXML.error === true) {
// Unable to parse as XML
} else {
// Continue processing
}

Access tags and their content using getElementsByTagName(), .text, and getAttribute():

let outer = parsedXML.getElementsByTagName('outer')[0];
let inner = outer.getElementsByTagName('inner')[0];

let innerText = inner.text; // tag value
let attrValue = inner.getAttribute('my-attribute'); // attribute value

Every DOMNodeList has a length property, which lets you iterate over elements when the XML structure isn't known in advance.

Troubleshooting

My custom security check doesn't run during the scan

Check the following:

  • The check is uploaded and visible under Scans > Custom security checks.
  • The scan profile you selected includes the check.
  • The file uses the .js extension.
The script runs but I can't find the log files

Log files are only generated when target debugging is enabled. Before launching the scan, open the target settings, go to the Advanced tab, and enable Debug scans for this target. After the scan completes, the log file path appears on the Events tab.

A vulnerability alert is assigned to / instead of the correct path

This happens when you pass a path value in addVuln() that Invicti isn't aware of. Either switch to location: scriptArg.location, or make sure the path was discovered during the scan or added via scanState.hintLinks() before the alert is issued.


Need help?

Invicti Support team is ready to provide you with technical help. Go to Help Center

Was this page useful?