Custom Integration Scripts

View as Markdown

Community Platform

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 CONFIG block 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_TLS and OPTIONS_HTTP) add standard connection controls to any script without copying boilerplate parameters.
  • report_assets streams assets to runZero page-by-page, so large imports no longer need to be held in memory.
  • matchBehavior and the trust_* flags give scripts control over how imported assets merge with existing inventory.
  • A much larger library of built-in modules, including typed kwargs accessors, 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 script CLI command gained a --validate mode that smoke-tests a script’s CONFIG and HTTP/TLS wiring against a local dummy server, and an --output mode 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:

  1. Write the script, starting with a CONFIG block and a main function.
  2. Test it locally with the runZero CLI (runzero script).
  3. Create the custom integration in the console and paste in the script.
  4. Create a credential; the form is generated from the script’s CONFIG parameters.
  5. 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:

  1. There is no exception handling (try/except). Use return values to signal errors.
  2. There is no f-string f'{var}' formatting. Use "{}".format(var) for string interpolation.
  3. 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]
  • args receives any positional arguments configured on the task, as strings.
  • kwargs receives the credential fields and task keyword arguments, as strings. Declare every key your script reads in CONFIG["params"], and read them with the typed kwargs accessors.
  • For inbound integrations, main returns a list of ImportAsset objects (or a single ImportAsset). Returning None is valid when assets are streamed with report_assets instead.
  • For outbound integrations, main performs its export work and returns None.

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 apply default values before main runs.
  • Route secret parameters 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:

  • CONFIG must 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 is CONFIG["includes"], which may reference the predeclared option-set identifiers OPTIONS_TLS and OPTIONS_HTTP.
  • Scripts without a CONFIG block continue to run with the legacy access_key and access_secret credential 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 kwargs under its key. Values are delivered as strings; use the kwargs module (get_string, get_int, get_bool, get_list, …) to read them with type coercion and defaults.
  • Declared default values are applied, enum aliases are normalized, and validation runs before main is 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_key and access_secret fields 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 --validate verifies the wiring: a script that declares OPTIONS_HTTP or OPTIONS_TLS but never passes the collected options to an HTTP call fails validation.
  • The stateful requests.Session object does not accept the tls= dict; only its insecure_skip_verify constructor flag is available. Prefer the http module 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:

  • matchBehavior accepts a space-separated string of flags built from no- + (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, and trust_os_version (booleans) apply the script’s deviceType, os, and osVersion values 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

  1. Go to the Custom Integrations page and click Add custom integration.
  2. Provide a name and optionally an icon.
  3. Toggle Enable custom integration script and paste in your script.
  4. Click Validate to check the script syntax and CONFIG block, then Save.

Step 2: Create the credential

  1. Go to the Credentials page and click Add Credential.
  2. Choose Custom Integration Script Secrets as the credential type.
  3. Select the custom integration. The form shows the fields declared in the script’s CONFIG block, including any option-suite fields such as tls_disable_validation and http_user_agent.
  4. For legacy scripts without a CONFIG block, provide Access Key and Access Secret values, which are passed to the script as the access_key and access_secret kwargs.
  5. To let other organizations use this credential, select the Make this a global credential option.
  6. Save the credential.

Step 3: Create the task

  1. Go to the custom integration task page, or click Integrate on the Tasks page and choose Custom Scripts.
  2. Select the custom integration and the credential created above.
  3. Select the Explorer to run the script from. Custom integration scripts always run on one of your Explorers, not from the runZero cloud.
  4. Set the site, description, and schedule as appropriate.
  5. 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.sleep calls.
  • HTTP response bodies are capped at 1 GiB; other modules enforce similar size caps (see the library reference).
  • Values of secret parameters are automatically redacted from print output, 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. Raw socket connections 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

NameSetup InstructionsIntegration Code
Akamai Guardicore CentraLinkLink
Audit Log to WebhookLinkLink
AutomoxLinkLink
BitsightLinkLink
Carbon BlackLinkLink
Cisco ISELinkLink
Cortex XDRLinkLink
CyberintLinkLink
Device42LinkLink
Digital OceanLinkLink
DrataLinkLink
Extreme Networks CloudIQLinkLink
Ghost SecurityLinkLink
HalcyonLinkLink
Ivanti NeuronsLinkLink
JAMFLinkLink
KandjiLinkLink
KubernetesLinkLink
LimaCharlieLinkLink
Linux via SSHLinkLink
ManageEngine Endpoint CentralLinkLink
MazeLinkLink
Microsoft SQL Server databasesLinkLink
MosyleLinkLink
NetskopeLinkLink
NexthinkLinkLink
NinjaOneLinkLink
ProxmoxLinkLink
Scale ComputingLinkLink
Scan Passive AssetsLinkLink
Snipe-ITLinkLink
Snow License ManagerLinkLink
SolarWinds Information ServiceLinkLink
StairwellLinkLink
Sumo LogicLinkLink
TailscaleLinkLink
TaniumLinkLink
Ubiquiti UniFi NetworkLinkLink
Vulnerability WorkflowLinkLink
WazuhLinkLink
Windows SMB sharesLinkLink
Windows WMILinkLink
exe.devLinkLink
pfSenseLinkLink
runZero Task SyncLinkLink
Updated