Custom Integration Scripts
Custom integration scripts let you import asset data from any source that runZero does not already integrate with, or export runZero data to another system. Scripts are written in Starlark, a Python-like language, and run in a sandbox on one of your Explorers.
runZero 5.1 significantly expanded what custom integration scripts can do:
- An embedded
CONFIGblock describes the integration and its parameters. The console uses it to generate a typed credential form, validate input, apply defaults, and route secrets through encrypted storage. - Shared TLS and HTTP option suites (
OPTIONS_TLSandOPTIONS_HTTP) add standard connection controls to any script without copying boilerplate parameters. report_assetsstreams assets to runZero page-by-page, so large imports no longer need to be held in memory.matchBehaviorand thetrust_*flags give scripts control over how imported assets merge with existing inventory.- A much larger library of built-in modules, including typed
kwargsaccessors, HTTP helpers with retry support, parsers for CSV/XML/streaming JSON, JWT and AWS SigV4 signing, and direct-protocol modules for SSH, SMB, WinRM, WMI, and SQL. - The
runzero scriptCLI command gained a--validatemode that smoke-tests a script’s CONFIG and HTTP/TLS wiring against a local dummy server, and an--outputmode that writes real scan output for inspection.
A library of ready-to-use integrations built on these capabilities is available in the runzero-custom-integrations GitHub repository, including a boilerplate template to start from.
Overview
To set up a custom integration script:
- Write the script, starting with a
CONFIGblock and amainfunction. - Test it locally with the runZero CLI (
runzero script). - Create the custom integration in the console and paste in the script.
- Create a credential; the form is generated from the script’s
CONFIGparameters. - Create an integration task to run the script on a schedule.
Writing a script
The Starlark language
Starlark is a dialect of Python with some notable differences:
- There is no exception handling (
try/except). Use return values to signal errors. - There is no f-string
f'{var}'formatting. Use"{}".format(var)for string interpolation. - The standard library is limited to the built-in modules provided by runZero.
The custom integration sandbox also disallows recursion, top-level if/for statements, and reassigning global variables. while loops and the set() type are supported.
The entrypoint
Every script must define a main function. runZero calls it with the task’s arguments and keyword arguments:
load('runzero.types', 'ImportAsset')
def main(*args, **kwargs):
asset = ImportAsset(
id='device-1',
hostnames=['web1.example.com'],
os='Linux',
osVersion='6.1',
manufacturer='Example Corp',
model='VM',
)
return [asset]
argsreceives any positional arguments configured on the task, as strings.kwargsreceives the credential fields and task keyword arguments, as strings. Declare every key your script reads inCONFIG["params"], and read them with the typedkwargsaccessors.- For inbound integrations,
mainreturns alistofImportAssetobjects (or a singleImportAsset). ReturningNoneis valid when assets are streamed withreport_assetsinstead. - For outbound integrations,
mainperforms its export work and returnsNone.
Streaming large imports with report_assets
Returning one large list from main keeps every asset, plus the raw API responses used to build them, in memory at once. For sources with large inventories, stream each page to runZero as it is processed instead. report_assets is a predeclared builtin, so no load() is needed:
load('runzero.types', 'ImportAsset')
def fetch_page(cursor):
# Replace with a real paginated API call.
if cursor == None:
return [{"id": "a-1"}, {"id": "a-2"}], "page-2"
return [{"id": "b-1"}], None
def main(*args, **kwargs):
total = 0
cursor = None
while True:
page, cursor = fetch_page(cursor)
if not page:
break
assets = [ImportAsset(id=item["id"]) for item in page]
total += report_assets(assets) # stream this page to runZero
if not cursor:
break
print("reported {} assets".format(total))
return None # nothing buffered in main
report_assets accepts a single asset, several positional assets, or a list/tuple of assets, and returns the number reported. Assets streamed with report_assets and assets returned from main are both imported, so partial adoption is safe.
The CONFIG block
The CONFIG block is an embedded, declarative description of the integration. When you save a script in the console, runZero extracts CONFIG (without executing the script) and uses it to:
- Render a typed credential form with labels, groups, and conditional fields.
- Validate submitted values (
required,min,max,pattern,options) and applydefaultvalues beforemainruns. - Route
secretparameters through encrypted storage and redact their values from script logs and error messages. - Refuse to run the script on runZero versions older than
minVersion.
CONFIG = {
"id": "runzero-example",
"name": "Example integration",
"type": "inbound",
"description": "Imports devices from the Example API.",
"version": "26081400",
"minVersion": "5.1.0",
"params": [
{"key": "url", "label": "Example API URL", "type": "url", "required": True},
{"key": "api_token", "label": "API token", "type": "secret", "required": True},
{"key": "page_size", "label": "Page size", "type": "int", "required": False,
"default": 100, "min": 1, "max": 1000},
],
"includes": {
"tls_": OPTIONS_TLS,
"http_": OPTIONS_HTTP,
},
}
load("runzero.types", "ImportAsset")
# ...rest of script
Rules for the block itself:
CONFIGmust be the first top-level statement in the script. Comments and blank lines may appear before it;load(...)calls, constants, and other statements may not.- Every value must be a literal: strings, numbers,
True/False/None, lists, tuples, and dicts with string keys. Function calls, variable references, and arithmetic are rejected. The only exception isCONFIG["includes"], which may reference the predeclared option-set identifiersOPTIONS_TLSandOPTIONS_HTTP. - Scripts without a
CONFIGblock continue to run with the legacyaccess_keyandaccess_secretcredential fields, so existing integrations keep working unchanged.
Top-level fields
| Field | Description |
|---|---|
id |
Stable lower-case identifier, for example runzero-tailscale. |
name |
Display name for the integration. |
type |
inbound, outbound, or internal (descriptive). |
description |
Short summary of what the integration does. |
version |
Integration version string, for example 26081400. |
minVersion |
Minimum runZero version required to run the script, for example 5.1.0. Older Explorers refuse to run the script and report a clear upgrade message. |
params |
List of parameter definitions. |
includes |
Shared option suites, keyed by prefix. |
atLeastOneOf |
List of parameter-key groups; at least one key in each group must be set. |
exactlyOneOf |
List of parameter-key groups; exactly one key in each group must be set. |
rejectUnknown |
Reject keys that are not declared in params. Unknown keyword arguments are always rejected at run time for CONFIG-based scripts. |
validationMode |
How runzero script --validate exercises the script: the default expects an HTTP request; "compile" only checks that CONFIG parses and main exists. Use "compile" for direct-protocol (SSH/SMB/WMI/WinRM/SQL) integrations. |
Parameter definitions
Each entry in params describes one credential form field and one keyword argument delivered to main.
Supported type values: string, secret, int, float, bool, enum (requires options), url, textarea, and json.
| Field | Description |
|---|---|
key |
Keyword argument name. Must match ^[a-zA-Z_][a-zA-Z0-9_]*$ and match the name the script reads. |
label |
Form label. |
description |
Help text shown under the field. |
type |
One of the types above. secret values get masked input, encrypted storage, and log redaction. |
required |
Whether the field must be set. |
default |
Default value applied when the field is left blank. Not allowed on secret parameters. |
placeholder |
Placeholder text for the form field. |
options |
Allowed values for enum parameters. |
multi |
For enum: allow multiple comma-separated selections. |
min / max |
Numeric bounds for int/float; length bounds for string/secret/textarea. |
pattern |
Regular expression the value must fully match (string, secret, textarea, url). |
aliases / caseInsensitive |
For enum: alternate spellings that normalize to a canonical option before main runs. |
dependsOn, visibleIf, visibleIfValue |
Show this field only when another declared field is set (optionally to a specific value). |
requiredIf, requiredIfValue |
Make this field required when another declared field is set (optionally to a specific value). |
group |
Section heading used to group fields in the form. |
How values reach your script
- Each declared parameter arrives in
kwargsunder itskey. Values are delivered as strings; use thekwargsmodule (get_string,get_int,get_bool,get_list, …) to read them with type coercion and defaults. - Declared
defaultvalues are applied, enum aliases are normalized, and validation runs beforemainis called. - CONFIG-based scripts reject unknown keyword arguments.
- Credential keys starting with
_(for example_integration_id) are stored on the credential but never forwarded to the script. - For backward compatibility, the credential’s
access_keyandaccess_secretfields are still injected when the task does not provide them.
Shared TLS and HTTP option suites
Most integrations talk to an HTTPS API, and most of them need the same connection controls: trusting a private CA, pinning a certificate, presenting a client certificate, disabling validation in a lab, or sending a specific User-Agent. Rather than declaring these parameters in every script, add the predeclared option suites to CONFIG["includes"]:
"includes": {
"tls_": OPTIONS_TLS,
"http_": OPTIONS_HTTP,
},
Each include expands into a set of parameters. The dict key ("tls_", "http_") is a prefix prepended to every generated parameter key, and the generated parameters appear in the credential form after the script’s own parameters.
OPTIONS_TLS
With the conventional tls_ prefix, OPTIONS_TLS generates these keyword arguments:
| Generated kwarg | Type | Default | Description |
|---|---|---|---|
tls_disable_validation |
bool |
False |
Allow connections to endpoints with untrusted TLS certificates. |
tls_ca_cert |
textarea |
— | PEM-encoded certificate authorities to trust when validating the endpoint certificate. |
tls_peer_hash |
string |
— | SHA-256 fingerprint of the endpoint certificate to trust (certificate pinning). Multiple pins can be separated by commas, semicolons, spaces, or newlines. |
tls_client_cert |
textarea |
— | PEM-encoded client certificate to present for mutual TLS. |
tls_client_key |
secret |
— | PEM-encoded private key for the client certificate. Required when tls_client_cert is set. |
OPTIONS_HTTP
With the conventional http_ prefix, OPTIONS_HTTP generates:
| Generated kwarg | Type | Default | Description |
|---|---|---|---|
http_user_agent |
string |
"" |
Optional User-Agent header sent with HTTP requests. |
Using the option suites
Declaring the includes only creates the form fields; the script must pass the collected values to the HTTP client. The kwargs module does this in one call:
load("http", "get_json", "bearer")
load("kwargs", "get_string", "get_url_base", "get_http_options")
def main(*args, **kwargs):
http_options = get_http_options(kwargs, headers={
"Authorization": bearer(get_string(kwargs, "api_token")),
"Accept": "application/json",
})
data, err = get_json("{}/v1/devices".format(get_url_base(kwargs)), **http_options)
get_http_options(kwargs, prefix="http_", tls_prefix="tls_", headers=None) gathers the suite values into a dict of keyword arguments (headers= and tls=) that can be splatted into any http module function (get, post, get_json, post_json, oauth2_token, and so on). The mapping is:
| Suite kwarg | Effect |
|---|---|
tls_disable_validation |
tls={"insecure": True} |
tls_ca_cert |
tls={"ca_pem": ...} |
tls_client_cert / tls_client_key |
tls={"client_cert_pem": ..., "client_key_pem": ...} |
tls_peer_hash |
tls={"thumbprints": [...]} |
http_user_agent |
headers={"User-Agent": ...} (only when the header is not already set) |
Use get_http_tls(kwargs, "tls_") when a script only needs the tls= dict and manages headers itself. The tls= dict can also be built by hand; it accepts the keys insecure, server_name, ca_pem, client_cert_pem, client_key_pem, and thumbprints, and rejects anything else.
Two things to be aware of:
runzero script --validateverifies the wiring: a script that declaresOPTIONS_HTTPorOPTIONS_TLSbut never passes the collected options to an HTTP call fails validation.- The stateful
requests.Sessionobject does not accept thetls=dict; only itsinsecure_skip_verifyconstructor flag is available. Prefer thehttpmodule functions when the TLS suite matters.
Multiple endpoints
Scripts that talk to more than one endpoint can include a suite more than once under different prefixes, and collect each set separately:
"includes": {
"src_tls_": OPTIONS_TLS,
"src_http_": OPTIONS_HTTP,
"dst_tls_": OPTIONS_TLS,
"dst_http_": OPTIONS_HTTP,
},
src_options = get_http_options(kwargs, "src_http_", "src_tls_", src_headers)
dst_options = get_http_options(kwargs, "dst_http_", "dst_tls_", dst_headers)
A complete example
The script below puts the pieces together: a CONFIG block with typed parameters and both option suites, option-suite plumbing via get_http_options, paginated fetching with get_json, and streaming import via report_assets. It passes runzero script --validate as-is, and can be used as a starting point for a real integration (see also the boilerplate template on GitHub).
CONFIG = {
"id": "runzero-example",
"name": "Example integration",
"type": "inbound",
"description": "Imports devices from the Example API.",
"version": "26081400",
"minVersion": "5.1.0",
"params": [
{"key": "url", "label": "Example API URL", "type": "url", "required": True},
{"key": "api_token", "label": "API token", "type": "secret", "required": True},
],
"includes": {
"tls_": OPTIONS_TLS,
"http_": OPTIONS_HTTP,
},
}
load("runzero.types", "ImportAsset", "to_custom_attributes")
load("net", "network_interface")
load("http", "get_json", "bearer")
load("kwargs", "require", "get_string", "get_url_base", "get_http_options")
def build_asset(device):
return ImportAsset(
id=str(device["id"]),
hostnames=[device.get("hostname", "")],
os=device.get("os", ""),
networkInterfaces=[network_interface(mac=device.get("mac"),
ips=device.get("ips", []))],
customAttributes=to_custom_attributes(device),
)
def main(*args, **kwargs):
require(kwargs, "url", "api_token")
base_url = get_url_base(kwargs)
http_options = get_http_options(kwargs, headers={
"Authorization": bearer(get_string(kwargs, "api_token")),
"Accept": "application/json",
})
total = 0
page = 1
while True:
data, err = get_json("{}/v1/devices".format(base_url),
params={"page": str(page)}, retries=2, **http_options)
if err:
print("request failed: {}".format(err))
break
devices = data or []
if not devices:
break
total += report_assets([build_asset(d) for d in devices])
page += 1
print("imported {} devices".format(total))
return None
runzero script --filename example-integration.star --validate
INFO example-integration.star validated with 1 HTTP request(s)
INFO validated 1 script(s) with dummy HTTP/TLS server https://127.0.0.1:52084
Controlling how assets merge
By default, imported assets merge with existing inventory using the asset id, MAC address, IP address, and hostname. The ImportAsset fields below adjust that behavior per asset:
matchBehavioraccepts a space-separated string of flags built fromno-+ (id|mac|ip|name) + (-match|-break). Two presets cover most cases:"no-mac-break no-ip-break no-name-break"— use when your source supplies a stable, unique id (vendor UUID, serial number). The id still drives merges, but differing MACs, IPs, or names will not disqualify a merge with an existing asset."no-id-match no-id-break"— use when your source only emits ephemeral or per-run ids. The id is ignored and merging falls back to MAC, IP, and hostname.
trust_device_type,trust_os, andtrust_os_version(booleans) apply the script’sdeviceType,os, andosVersionvalues to the asset fingerprint even when runZero cannot normalize them through its fingerprint engine.
Leave matchBehavior unset to keep the default matcher behavior, which is correct for most integrations. See the asset identity guidance in the custom integrations repository for a deeper treatment.
Testing scripts with the runZero CLI
The runZero CLI includes a script sub-command for developing and debugging integration scripts locally, using the same Starlark engine and modules as the Explorer.
runzero script [--filename file] [--args a] [--kwargs key=value]
runzero script repl [--filename file]
| Flag | Description |
|---|---|
-f, --filename |
Script file to load and run. --validate also accepts a directory. |
--args |
Positional argument passed to main (repeatable). |
--kwargs key=value |
Keyword argument passed to main (repeatable). |
--validate |
Validate the script CONFIG and smoke-test HTTP/TLS wiring against a local dummy server. |
-o, --output |
Directory to write scan output to (scan.runzero.gz). |
--overwrite |
Replace the output directory if it already exists. |
--custom-integration-id |
Integration UUID stamped on exported records; required with --output. |
Running scripts
Save a minimal script as hello.star:
def main(*args, **kwargs):
print("Hello {}!".format(kwargs.get("name", "world")))
runzero script --filename hello.star --kwargs name=Dave
INFO script: Hello Dave!
INFO script completed successfully with 0 returned asset(s)
--args and --kwargs can be repeated to pass as many values as needed:
runzero script --filename hello.star --args one --args two --kwargs api_token=foo --kwargs url=https://api.example.com
Note that a local run calls main with exactly the arguments you provide; CONFIG defaults and validation are applied by the console and Explorer at task time, and by --validate locally.
Validating scripts
--validate checks a script end-to-end without touching the real vendor API:
runzero script --filename example.star --validate
INFO example.star validated with 1 HTTP request(s)
INFO validated 1 script(s) with dummy HTTP/TLS server https://127.0.0.1:52084
For HTTP integrations, validation parses the CONFIG block, generates type-appropriate placeholder values for every parameter, initializes the script, calls main, and transparently routes all HTTP requests to a local TLS server that returns canned responses. It fails if the script never makes an HTTP request, and if declared OPTIONS_HTTP/OPTIONS_TLS options never reach the HTTP client.
Scripts with "validationMode": "compile" (templates and direct-protocol integrations) stop after verifying that CONFIG parses and main exists.
Passing a directory validates every .star file under it:
runzero script --filename ./my-integrations/ --validate
A successful validation is a CONFIG and wiring check, not proof that the vendor API accepts your credentials or returns the expected payload — test against the real API with --kwargs for that.
Exporting scan output
To inspect exactly what a script would import, write real scan output to a directory:
runzero script --filename example.star --kwargs api_token=MY_TOKEN -o ./out --overwrite --custom-integration-id 11111111-2222-3333-4444-555555555555
This writes ./out/scan.runzero.gz, a gzip-compressed stream of JSON records — one per asset — in the same format an integration task produces. Without --output, assets streamed via report_assets are discarded with a warning and only counted.
REPL
runzero script repl starts an interactive session with all modules preloaded. With --filename, the script’s functions and globals are available to call directly:
$ runzero script repl --filename hello.star
>>> main(**{"name": "Dave"})
INFO script: Hello Dave!
>>> load('json', json_decode='decode')
>>> data = json_decode('{"greeting": "hello"}')
>>> print(data["greeting"])
INFO script: hello
Exit the REPL with ^D.
Adding the integration to your console
Step 1: Create the custom integration
- Go to the Custom Integrations page and click Add custom integration.
- Provide a name and optionally an icon.
- Toggle Enable custom integration script and paste in your script.
- Click Validate to check the script syntax and CONFIG block, then Save.
Step 2: Create the credential
- Go to the Credentials page and click Add Credential.
- Choose Custom Integration Script Secrets as the credential type.
- Select the custom integration. The form shows the fields declared in the script’s
CONFIGblock, including any option-suite fields such astls_disable_validationandhttp_user_agent. - For legacy scripts without a CONFIG block, provide Access Key and Access Secret values, which are passed to the script as the
access_keyandaccess_secretkwargs. - To let other organizations use this credential, select the Make this a global credential option.
- Save the credential.
Step 3: Create the task
- Go to the custom integration task page, or click Integrate on the Tasks page and choose Custom Scripts.
- Select the custom integration and the credential created above.
- Select the Explorer to run the script from. Custom integration scripts always run on one of your Explorers, not from the runZero cloud.
- Set the site, description, and schedule as appropriate.
- Activate the connection to start the task.
Once the task completes, assets appear in your inventory and can be found with the search custom_integration:<name>.
Script limits and sandbox behavior
- Scripts are limited to 1 MiB of source.
- Scripts can only
load()the registered modules; there is no filesystem access or relative import. - A script run is bounded by an execution-step ceiling and a 24-hour wall clock; the deadline also bounds HTTP requests and
time.sleepcalls. - HTTP response bodies are capped at 1 GiB; other modules enforce similar size caps (see the library reference).
- Values of
secretparameters are automatically redacted fromprintoutput, progress messages, and error text. - The
http,requests, and authenticated protocol modules (runzero.ssh,runzero.smb,runzero.winrm,runzero.wmi,runzero.sql) can reach internal addresses visible to the Explorer the task runs on. Rawsocketconnections are blocked from connecting to private and internal IP addresses.
Available libraries
Load only what you use; each module is available via load(...). Full signatures and runnable examples for every module are on the Starlark libraries page.
| Module | Provides |
|---|---|
runzero.types |
ImportAsset, NetworkInterface, Service, ServiceProtocolData, Software, Vulnerability, to_custom_attributes |
kwargs |
Typed accessors: require, get_string, get_bool, get_int, get_float, get_list, get_url_base, get_http_tls, get_http_options |
http |
HTTP verbs, get_json/post_json with retries, bearer/basic/oauth2_token, url_encode/url_parse/url_join, multipart |
requests |
Stateful HTTP Session with sticky headers and cookies |
net |
ip_address, network_interface, normalize_mac, ip_network, ip_in_network, resolve |
json |
encode, decode, encode_indent, indent |
jsonstream |
iter_array, iter_lines for large JSON/NDJSON responses |
csv |
read_all, read_rows, write_all, write_dicts |
xml |
parse into an element tree |
re |
RE2 regular expressions: match, find_all, sub, split, compile |
time |
now, parse_time, parse_duration, from_timestamp, sleep |
uuid |
new_uuid |
base64 / hex / base32 |
Standard encodings, including raw and URL-safe variants |
crypto |
Hashes, HMAC, AWS SigV4 signing, CSPRNG output |
jwt |
encode, decode, decode_unverified |
gzip |
compress, decompress |
flatten_json |
flatten nested structures |
runzero.progress |
report, info, warn task progress in the console |
socket |
Raw TCP/UDP/TLS connections |
runzero.ssh / runzero.smb / runzero.winrm / runzero.wmi / runzero.sql |
Direct-protocol collection from systems without a REST API |
The predeclared names report_assets, OPTIONS_TLS, and OPTIONS_HTTP are always available without a load().
Existing Custom Integrations
| Name | Setup Instructions | Integration Code |
|---|---|---|
| Akamai Guardicore Centra | Link | Link |
| Audit Log to Webhook | Link | Link |
| Automox | Link | Link |
| Bitsight | Link | Link |
| Carbon Black | Link | Link |
| Cisco ISE | Link | Link |
| Cortex XDR | Link | Link |
| Cyberint | Link | Link |
| Device42 | Link | Link |
| Digital Ocean | Link | Link |
| Drata | Link | Link |
| Extreme Networks CloudIQ | Link | Link |
| Ghost Security | Link | Link |
| Halcyon | Link | Link |
| Ivanti Neurons | Link | Link |
| JAMF | Link | Link |
| Kandji | Link | Link |
| Kubernetes | Link | Link |
| LimaCharlie | Link | Link |
| Linux via SSH | Link | Link |
| ManageEngine Endpoint Central | Link | Link |
| Maze | Link | Link |
| Microsoft SQL Server databases | Link | Link |
| Mosyle | Link | Link |
| Netskope | Link | Link |
| Nexthink | Link | Link |
| NinjaOne | Link | Link |
| Proxmox | Link | Link |
| Scale Computing | Link | Link |
| Scan Passive Assets | Link | Link |
| Snipe-IT | Link | Link |
| Snow License Manager | Link | Link |
| SolarWinds Information Service | Link | Link |
| Stairwell | Link | Link |
| Sumo Logic | Link | Link |
| Tailscale | Link | Link |
| Tanium | Link | Link |
| Ubiquiti UniFi Network | Link | Link |
| Vulnerability Workflow | Link | Link |
| Wazuh | Link | Link |
| Windows SMB shares | Link | Link |
| Windows WMI | Link | Link |
| exe.dev | Link | Link |
| pfSense | Link | Link |
| runZero Task Sync | Link | Link |