Wasm Plugins
ServiceRadar supports sandboxed WebAssembly (Wasm) plugins for custom checkers and integrations. Plugins are uploaded or imported through the web UI, reviewed for capabilities and allowlists, and then assigned to agents. Agents run plugins in an embedded Wasm runtime (wazero) with strict resource limits and a capability-based host ABI.
This page is an operator-facing conceptual overview. For the full plugin SDK and authoring reference — manifest fields, config and result schemas, the host ABI, code examples, and build instructions — see the developer portal at developer.serviceradar.cloud.
Why Wasm Plugins
Wasm plugins let ServiceRadar extend its checking capabilities without trusting arbitrary native code on the edge:
- Sandboxed. Each plugin runs inside an isolated Wasm runtime. It cannot touch the host filesystem, network, or processes directly.
- Capability-based. A plugin can only do what its manifest explicitly declares and an operator explicitly approves. Every host call is mediated and enforced.
- Resource-limited. The agent enforces per-plugin budgets for memory, CPU time, and open connections.
- Portable. Plugins compile to a single
wasm32-wasiartifact that runs identically across agent platforms.
The current edge model is push-based: the agent streams results to agent-gateway. External "pull" checkers are not part of the primary architecture; prefer Wasm plugins or first-party collectors that publish into the normal pipelines.
Wasm is also how ServiceRadar ships certain first-party checks. For example, the Dusk checker runs as a Wasm plugin executed by serviceradar-agent rather than as a standalone service. Wasm plugins are one part of the edge runtime — the agent also runs embedded engines (sync integrations, SNMP polling, discovery/mapping, mDNS) alongside plugins.
Package Format
Each plugin package is made up of:
plugin.yaml— the manifest (plugin identity, capabilities, permissions, resource requests)plugin.wasm— the compiled Wasm binary- optional sidecars such as a config JSON Schema, result display contract, or log/event signal display contracts
The control plane stores the manifest and config schema in the database and stores the Wasm binary in the configured package storage backend.
The exact manifest fields, the supported config JSON Schema subset, and the serviceradar.plugin_result.v1 result schema are documented in full on the developer portal.
Package-declared integrations
External inventory plugins own their provider-specific configuration, operator
documentation, credential profile, schedule binding, inventory source label,
and source metadata display fields. Put the documentation under docs/, define
the configuration controls in config.schema.json, and publish the bounded
declarative contract under integrations in plugin.yaml:
producer_schedules:
- schedule_id: example-inventory.refresh
label: Refresh example inventory
action_id: example-inventory.refresh
command_type: plugin.run_action
default_cadence_seconds: 86400
min_cadence_seconds: 3600
max_cadence_seconds: 2592000
credential_requirements:
inventory_account:
required: true
resolution_location: agent
grants: []
integrations:
documentation:
title: Example inventory configuration
path: docs/configuration.md
credential_profiles:
- provider: example-inventory
label: Example Inventory
auth_methods:
- id: username_password
credential_kind: username_password
purposes: [device_inventory]
scope_types: [agent]
provisioning:
mode: producer_schedule
schedule_id: example-inventory.refresh
credential_requirement: inventory_account
inventory_sources:
- source: example-inventory
label: Example Inventory
metadata_fields:
- key: site
label: Site
One credential rule, several schedules
A producer_schedule profile may bind more than one schedule of the same
package, so one vendor account drives, say, an inventory refresh every 15
minutes and a telemetry poll every minute. Replace schedule_id with
schedule_ids:
producer_schedules:
- schedule_id: example-inventory.refresh
default_cadence_seconds: 900
credential_requirements:
inventory_account: {required: true, resolution_location: agent, grants: []}
# ...label, action_id, command_type, bounds as above
- schedule_id: example-inventory.telemetry
default_cadence_seconds: 60
min_cadence_seconds: 30
credential_requirements:
inventory_account: {required: true, resolution_location: agent, grants: []}
# ...
integrations:
credential_profiles:
- provider: example-inventory
# ...
provisioning:
mode: producer_schedule
schedule_ids:
- example-inventory.refresh
- example-inventory.telemetry
credential_requirement: inventory_account
Rules the importer enforces:
- Declare exactly one of
schedule_idorschedule_ids.schedule_idkeeps working unchanged. schedule_idsis a non-empty list of at most 8 distinct ids. Every id must name a schedule in the same package'sproducer_schedules, and every listed schedule must declare the profile'scredential_requirement.
What one rule then provisions:
- One assignment per agent, as before. Every listed schedule is bound to that assignment with the same plugin config and the same credential reference. The dispatcher runs each schedule independently.
- Per-schedule cadence. The first listed id is the primary schedule. The
rule form's cadence field is bounded by the primary's
min_cadence_seconds/max_cadence_secondsand overrides the primary only. Every other schedule runs at its owndefault_cadence_seconds; the override is never copied onto a schedule with a different cadence contract. - Shared on/off. The rule's recurring-refresh switch arms and disarms all of
its schedules together, and disabling the rule, revoking the package, or
removing an id from
schedule_idsin a new package version disables the affected schedules on the next credential reconciliation. - Run Now on the credential rules page dispatches the primary schedule.
ServiceRadar validates this data while importing the signed package and builds
the credentials UI, assignment reconciliation, schedule binding, and discovery
source display from it. Adding another provider does not require a core catalog
entry, provider module, documentation page, or workflow. The protected
external-wasm-plugin.yml workflow accepts any repository matching the
configured external plugin namespace and packages conventional docs/,
display/, and schemas/ resources with the module.
Provider code is never loaded into the control plane. Core consumes only the
validated descriptor, JSON Schema, generic discovery envelope, and nested
source_metadata; duplicate provider/source claims and attempts to replace a
reserved built-in provider are rejected.
Northbound actions on discovered devices
A package that declares inventory_sources and northbound actions receives,
on every device and interface target, attributes.integration_ids: the
target device's integration_id identifiers whose <source>: prefix is one of
the package's own declared sources, sorted and deduplicated. Identifiers of
other sources are never included. These system-supplied identifiers remain in
the dispatch payload even when the action declares a target-field allowlist
that omits attributes.
An action credential requirement can take its secret from the package's own provisioned credentials instead of naming one:
actions:
- action_id: example-inventory.move_device
label: Move device between accounts
scopes: [device]
safety_classification: destructive
requires_confirmation: true
input_schema:
type: object
properties:
destination_rule_id: {type: string}
credential_requirements:
source_account:
credential_source: assignment_schedule
requirement: inventory_account
required: true
allow: {methods: [POST], hosts: [api.example.com]}
destination_account:
credential_source: package_rule
rule_input: destination_rule_id
required: true
allow: {methods: [POST], hosts: [api.example.com]}
assignment_schedule uses the secret bound under credential_refs[<requirement>]
of the enabled producer schedule on the plugin assignment the action runs on;
requirement must be the provisioning.credential_requirement of one of the
package's producer_schedule credential profiles. package_rule uses the
credential rule whose id the operator selects in the rule_input field, which
must name a string property in the action's input_schema. The package must
declare a producer_schedule credential profile. Only enabled rules
provisioned for the same package and provider are candidates; when the action
requirement declares purpose, the rule's purpose must match it exactly.
The action form lists candidate rules by name. Selecting a rule for a
user-launched action requires settings.credentials.manage as well as the
permission to launch the action. At grant preparation, the selected rule must
cover the dispatch assignment's agent and every target device through its
agent, gateway, or partition scope and its device target query. Invalid queries
and unsupported query filters are rejected rather than ignored; supported
filters are defined by ServiceRadar.SRQLDeviceMatcher.filters_supported?/1.
The form's candidate list does not guarantee that a rule covers the selected
targets; launch and poll grant preparation enforce that boundary.
If there are no candidate rules, or the lookup fails, the form displays that
state and disables submission when the credential input is required. An
optional package_rule credential can be omitted, in which case no grant is
issued for it. required: true on a package_rule requirement makes its input
required even when the input schema does not list it in required.
Neither source may be combined with credential_secret_id, secret_ref or any
secret input key, so an action input can never select arbitrary credential
material. An unresolved schedule binding,
an invalid supplied rule, or a missing required rule fails grant preparation
before the command is dispatched.
Pre-production validation
Validate a new inventory integration in a non-production partition before enabling its recurring schedule:
- Run the plugin repository's complete verification target, including unit tests, static analysis, TinyGo compilation, deterministic bundle reproduction, and vulnerability scanning.
- Publish an exact release tag through the protected generic workflow. Import and approve the signed package, then confirm that its configuration fields, credential profile, documentation, source labels, and disabled schedule all come from the package descriptor.
- Assign the package to one test agent with a least-privilege credential rule and use Run Now. Confirm that the command status contains only bounded identifiers, counts, and hashes and that logs, results, and audit events do not contain credentials or bearer tokens.
- Compare the source-inventory API result with a current provider export. Verify row counts, stable source object and integration IDs, declared metadata, and canonical DIRE matches. Replaying the same collection must be idempotent; a later complete collection may mark omitted observations absent but must not delete canonical devices.
- Run two collections at a shortened approved cadence, then restore the intended cadence. Verify recurring and Run Now executions use the same assignment, credential, RBAC, audit, timeout, and result-ingestion path.
- Exercise rollback by disabling the schedule or revoking the package and confirming that no further command can be dispatched while existing source observations remain auditable.
Plugins that emit OCSF events or OTEL-style logs must also declare
signal_schemas in plugin.yaml. Each signal schema points at a payload JSON Schema
and a declarative display contract shipped with the same package version. See
Telemetry Display Contracts for the operator
review model and fallback behavior.
Use the emit_telemetry capability for first-class plugin events, logs, or
metric batches that should be ingested independently of the check result. Metric
time-series must use the canonical serviceradar.metric.v1 telemetry payload;
serviceradar.plugin_result.v1 metrics are no longer a metric ingestion path.
Check-scoped annotations can still use the events field in
serviceradar.plugin_result.v1, but those events are coupled to submit_result
and are not a streaming telemetry surface.
Condition events and condition scopes
A Wasm plugin keeps no state between runs, so a check that reports a resource's
health emits the same condition every run. The agent de-duplicates these for
the plugin. An ocsf_event telemetry record is a condition event when its
unmapped object carries:
condition_key(string): a stable key for the condition, for exampleexample:<device_ref>:<alert_name>.level(string):ok,warning, orcritical. Whenunmappedalso carries numericratio,warn, andcrit, the agent applies hysteresis so a value parked at a threshold does not flap between levels.
Per assignment and key, the agent forwards a condition event the first time it sees the key, when the level changes, and at most once every 15 minutes while the level is unchanged. Other repeats are accepted and dropped. State for a key that is not observed for an hour is forgotten.
Those rules forward the first ok of every key. A plugin that mirrors a
vendor's list of active alerts would have to emit ok for every possible alert
on every device each run, or emit only active alerts and never deliver the
clear. Condition scopes solve this. Add unmapped.condition_scope (string, for
example example:<source_instance>:alerts) to each condition event, emit only
the active (non-ok) conditions, and close each run with one scope-complete
marker record for the scope:
{
"class_uid": 1008,
"category_uid": 1,
"type_uid": 100801,
"activity_id": 1,
"severity_id": 1,
"message": "condition scope snapshot",
"unmapped": {
"condition_scope_complete": "example:source-01:alerts",
"active_condition_keys": ["example:dev-01:thermal_throttle"]
}
}
active_condition_keys lists every key in the scope that the plugin currently
considers non-ok; it is required and may be empty. The marker may be emitted in
the same emit_telemetry call as the conditions or in a later call of the same
run. For scoped conditions the agent:
- Forwards non-ok levels with the rules above and remembers the last forwarded record for each key.
- Never forwards or refreshes
okfor a key it has not seen at a non-ok level. - Forwards
okonce for a key it remembers at a non-ok level, then forgets the key. - On a marker, synthesizes one
okevent for each remembered non-ok key in the scope that the marker does not list, then forgets those keys. The clear copies the class, device, andunmappedfields of the last forwarded event, withlevelset took, informational severity, the messageCondition cleared: <condition_key>, a new event id and time, andunmapped.condition_cleared_byset tocondition_scope_complete. Clears are placed directly after the marker, sorted bycondition_key. - Ignores marker keys it has never seen, and forwards the marker itself unchanged.
Emit the marker only when the run observed the whole scope. A run that
collected partially must omit the marker: no marker means no clears. Scoped
state is in memory and is forgotten after an hour without observations, and
forgetting a key never synthesizes a clear. An alert that clears while the
agent is restarting is therefore not cleared by the agent. Events without
condition_scope keep the unscoped behavior.
Gateway-Mediated Artifacts
Wasm plugins can produce more than small health-check results. A plugin that needs durable snapshots, advisory feed batches, SBOM evidence, or other large artifacts should use the host SDK artifact APIs. Those calls are logical ServiceRadar operations such as opening an artifact, writing chunks, committing with metadata, aborting, and reporting the committed object identity in the plugin result.
The plugin never receives NATS JetStream Object Store credentials and never talks to web-ng directly. The agent brokers the host call through agent-gateway, and agent-gateway writes through the normal internal object-storage path. Native add-ons follow the same boundary. Choose a native add-on only when the producer needs OS or runtime capabilities outside the Wasm sandbox, not because it needs durable artifact staging.
For vulnerability or threat-intelligence feeds, the producer is responsible for provider-specific download, schema validation, checksum verification, archive handling, and normalization. The result submitted to ServiceRadar should be the generic advisory batch contract plus snapshot provenance. Core stores and matches that generic contract; it does not own CISA, NVD, VulnCheck, OSV, or other provider parsers.
Scheduled advisory or diagnostic producers declare producer_schedules in the
plugin package manifest. ServiceRadar persists those declarations, renders
operator-owned settings for cadence, credentials, and assignment, and dispatches
due runs through the existing agent commandbus with plugin.run_action. The
scheduled invocation payload uses serviceradar.producer_schedule_run.v1; the
plugin remains responsible for provider-specific fetch and normalization.
Operator-selected credentials are converted into scoped credential_brokers in
the command payload. Raw credential_refs remain platform state and are not sent
directly to the agent.
capabilities:
- get_config
- submit_result
- http_request
- artifact-staging:v1
- advisory-feed:v1
- producer-schedule:v1
producer_schedules:
- schedule_id: daily_advisory_refresh
label: Refresh advisory feed
action_id: advisory.refresh
command_type: plugin.run_action
default_cadence_seconds: 86400
min_cadence_seconds: 3600
max_cadence_seconds: 2592000
jitter_seconds: 120
dispatch_scope: assignment
payload_template:
feed_key: primary
Capability and Permission Model
Capabilities and permissions are the core of the plugin security model. They are declared in the manifest and approved during import review. The agent enforces both the capability list and the permission allowlists on every host call.
- Capabilities name the host functions a plugin is allowed to call — for example, retrieving its config, writing agent runtime logs, emitting first-class telemetry, submitting a result, making HTTP requests, or opening TCP/UDP connections. A plugin cannot call a host function it did not declare.
- Permissions are the allowlists that scope those capabilities — for example, the set of HTTP hostnames, CIDR networks, and ports a plugin may reach. Network access is denied by default and only widened by explicit allowlist entries.
Because capabilities and permissions are visible in the manifest, reviewers can see a plugin's full blast radius before approving it. Always confirm them during import review, especially for plugins assigned to customer edge agents or networks that can reach sensitive systems.
The full list of capability names and permission keys lives on the developer portal.
Host-proxied unary gRPC (grpc_request)
A plugin that declares the grpc_request capability can call the grpc_unary
host function to make one unary gRPC call. The guest passes an already-serialized
request message and receives the serialized response message; the agent does not
need the service's .proto definition. Streaming RPCs are not supported.
The agent applies the same checks as http_request, before it dials:
- The destination host must be permitted by
allowed_domains(hostnames) orallowed_networks(IP literals), and the port must be listed inallowed_ports. Otherwise the call is denied and nothing is dialed. - Plaintext HTTP/2 (
transport: h2c) is only allowed when the destination resolves to an address insideallowed_networks; the call is pinned to that address. Usetransport: tlsfor anything else. TLS verifies against the same trust roots the agent uses for plugin HTTPS. - The call counts against
max_open_connectionswhile it runs. - The default timeout is 10 seconds and covers DNS lookup, dial, and the RPC
together. The response message is capped at 4 MiB, or lower when the request
sets
max_response_bytes. - Request metadata keys are lowercased. Pseudo-headers,
grpc-*keys, and transport headers such ascontent-typeandteare rejected. Keys ending in-bincarry base64 values.
A completed RPC, including a non-OK gRPC status, returns the status code,
message, headers, and trailers to the plugin. A connection that fails before
any gRPC status is reported as UNAVAILABLE (14).
capabilities:
- get_config
- submit_result
- grpc_request
permissions:
allowed_networks:
- 192.0.2.0/24
allowed_ports:
- 9200
Agents that support this capability advertise grpc_request.
SDKs and Authoring
Plugins compile to wasm32-wasi and export a zero-argument entrypoint that matches the manifest. ServiceRadar publishes SDKs that provide a higher-level API over the host ABI so you do not have to work with raw host imports.
See the SDKs & Plugin Development overview for a summary of the available SDKs, and the developer portal for the complete authoring reference, code examples, and build instructions.
Upload and Import Workflow
The plugin lifecycle is operator-facing and gated by an approval step:
- Upload or import a plugin package in the admin UI.
- The package is staged and must be approved before it can be used.
- During review, confirm the requested capabilities, permissions, and resource budget.
- Approved packages can be assigned to agents.
- Agents download packages only from the ServiceRadar control plane — never directly from GitHub.
Scheduled inventory plugins are the exception to step 4. A package that
declares producer_schedules and a credential profile with
provisioning.mode: producer_schedule (OpenText NOM today) is not enabled
from Assign to Agent. Import and approve it, then create the service-account
credential and rule under Settings -> Networks -> Credential Rules. The
rule's Scope Value is the agent that runs the Wasm module; saving the rule
creates the assignment. See OpenText NOM Inventory.
Plugin blob upload and download tokens are transported only in explicit headers or POST bodies. Query-string bearer tokens are not supported.
Publishing from the CLI
A developer can push a build straight to an instance with
@carverauto/serviceradar-cli, instead of uploading through the admin UI. The
package still lands staged and still needs an administrator's approval -- the CLI
replaces the upload step, not the review.
npx @carverauto/serviceradar-cli plugin init my-probe --template go
cd my-probe
tinygo build -target=wasi -no-debug -o plugin.wasm ./
npx @carverauto/serviceradar-cli plugin validate
npx @carverauto/serviceradar-cli auth login --instance https://serviceradar.example.com --scope plugin.publish
npx @carverauto/serviceradar-cli plugin publish --instance https://serviceradar.example.com
plugin init scaffolds against the language SDKs: --template go builds with
TinyGo against serviceradar-sdk-go, --template rust targets wasm32-wasip1
against serviceradar-sdk-rust. plugin validate checks plugin.yaml against
the same manifest contract the server enforces and makes no network calls.
github.com/carverauto/* modules, including serviceradar-sdk-go, are not
served by the public Go proxy or checksum database. Set both variables below for
every go get, go mod download, go mod vendor, and tinygo build invocation
that resolves modules, so Go fetches them directly from GitHub instead of failing
against the proxy/checksum database:
export GOPRIVATE='github.com/carverauto/*'
export GONOSUMDB='github.com/carverauto/*'
go get github.com/carverauto/serviceradar-sdk-go/v2@latest
The module path carries the /v2 major-version suffix. First-party plugins and
the plugin init --template go scaffold commit a vendor/ tree, so a build from
that tree needs no module download; the same two settings are used by the Bazel
plugin build and the CI workflows.
Publishing does three calls: it stages the package, requests a short-lived
storage token, then uploads the plugin.wasm bytes with that token. Track the
result with plugin status --id <package-id>, which reports the approval state
and, once approved, the capabilities that were actually granted -- an
administrator can approve a narrower set than the manifest requested.
A direct upload needs no signing key. allow_unsigned_uploads is on by default,
and the control on an uploaded package is the staged review with its
requested-versus-approved capability diff.
Token scope. auth login --scope plugin.publish mints a token that can reach
the plugin publish endpoints and nothing else; a token minted for
dashboard.publish is refused there, and vice versa. Request both with
--scope "dashboard.publish plugin.publish" if you publish both kinds of
package. The scope only makes an operation requestable -- the account still
needs the plugins.stage permission, and an operator controls which scopes the
CLI may request at all in Settings -> CLI auth policy.
Assigned health-result plugins, including first-party plugins such as UniFi and AlienVault OTX, appear in /services with a stable plugin service identity. When an assignment is created, the control plane seeds a pending service row; the next agent-reported plugin result updates that row with the plugin status and summary.
Authenticated partition binding and legacy recovery
An operator selects an agent, not a partition. Before an assignment is saved,
ServiceRadar displays the agent's Authenticated partition when a live mTLS
control session can prove it. The server derives the assignment partition from
that session again when it writes the assignment. Do not expect a partition
drop-down or attempt to add partition_id to an API request: an operator-supplied
value is never authority to route a plugin into an edge partition.
If the agent is offline, enrolled in more than one currently-live partition, or its control-session identity cannot be verified, the assignment fails closed. Bring the intended agent online and resolve the identity condition before trying again. A displayed partition is informational; it is checked again on save so a reconnect between viewing the form and confirming it cannot redirect the work.
Older deployments may contain a disabled assignment marked Unbound legacy assignment -- reapproval required. This is expected after the partition-binding migration: ServiceRadar intentionally did not infer an old assignment's partition from current inventory metadata.
The Plugins index has a Legacy recovery candidates table scoped to the current
workspace. It shows the affected agent, plugin package, recovery kind, safe
status, and a Review link. The queue is bounded to 50 cursor-backed rows per
page and shows only rows that still need action; a successfully reapproved or
reconciled historical row leaves the queue and retains only its safe completion
state in package detail. Treat it as a review queue, not a bulk repair tool: it
deliberately has no bulk enable action, deep offset scan, or default
partition assumption.
- Use Review, confirm the target agent is connected, and verify the package is still approved.
- For a manually owned assignment, use Reapprove and explicitly confirm the replacement. ServiceRadar creates a new, partition-bound assignment and leaves the historical row disabled for audit.
- For a policy- or credential-rule-owned assignment, use Reconcile policy. Do not manually clone its old configuration. Reconciliation re-evaluates the currently enabled owner, target scope, package, schema, and live agent identity before it can materialize a new assignment. The status refreshes while it is queued or running and then shows a safe terminal outcome; refresh the package detail if the browser session ends before it completes.
- Wait for the agent's next plugin result, then verify the restored service in
/servicesand the assignment detail view.
Manual recovery requires plugin-assignment permission. Policy recovery requires
current authority for its owning source; credential-rule recovery also requires
credential-management permission. If that credential permission is absent, the
reconciliation action is disabled and rejects a direct browser submission with
the same missing-permission explanation. The control plane rechecks all
authority before executing, so browser state never grants access by itself. The
recovery UI shows only a safe state: Reapproved for a completed manual row, or
a normalized policy state and replacement count for a policy row. Raw recovery
audits and durable recovery requests remain internal; the UI does not show
request parameters, principals, owner metadata, replacement IDs, secret values,
tokens, or private material. Do not re-enable unbound rows with direct database
updates.
Do not alter or delete them through raw database access; use the audited recovery
actions so the historical row remains available for investigation.
First-party plugin import
ServiceRadar ships first-party Wasm plugins as signed artifacts published by release automation. The Plugins UI can sync a first-party plugin index, verify the referenced signed bundle, mirror the Wasm payload into ServiceRadar-managed plugin storage, and stage the package for normal capability review. Imported first-party packages are not assignable until an authorized operator approves them.
Third-party plugin repositories
The Plugins UI imports from a plugin repository: a record naming a GitHub
repository, the release asset holding its plugin index, and the ed25519 key its
bundles must verify against. The built-in carverauto/serviceradar source is
seeded as one of these records; it can be disabled but not edited or removed.
Adding a repository requires the plugins.repositories.manage permission, which
is separate from plugins.stage on purpose: staging imports from a source the
platform already trusts, while adding a repository decides which sources are
trusted. Every add, edit, enable, disable and removal is written to the audit
log with the actor, the repository URL and its signing key id.
What a repository must publish
A release carries:
- the plugin index asset (default
serviceradar-wasm-plugin-index.json), whose entries name each plugin's id, version,bundle_url,bundle_digestandupload_signature_url; - the bundle zip for each entry;
- an ed25519 upload-signature document per bundle.
Bundles are signed with build/wasm_plugins/upload_signature_tool.go, a
dependency-free Go binary that cross-compiles to macOS, Windows and Linux and
reads its key from an environment variable or a file. Cosign is not required
for a third-party repository. Cosign applies only to the first-party OCI artifact
path, which additionally requires a public Rekor transparency-log entry.
The repository record stores the matching key_id and base64 public key, and a
bundle is verified against that repository's key -- so a bundle signed by one
publisher cannot be imported through another's catalog. A repository cannot be
saved without a key: a source with no trust anchor could never import anything,
so the failure belongs where a human can fix it.
Private repositories
Attach a GitHub personal access token to the repository. A fine-grained token with read-only Contents access to that one repository is enough. The token is stored encrypted in the credential store, is never returned by any read of the repository, and never appears in an audit record -- the UI shows only whether one is attached.
Two behaviours worth knowing:
- GitHub answers 404, not 403, for a private repository a token cannot see. A missing or expired token therefore looks identical to a missing release, so the error messages name both possibilities.
- Private release assets download through the API endpoint, which redirects to a short-lived pre-signed URL. ServiceRadar does not forward the token to that redirect target: the pre-signed URL carries its own authorization, and sending the token would disclose it to a host that has no need for it.
Sync
Each enabled repository syncs independently. One unreachable source -- an expired token, a repository that moved -- does not stop the others from importing; its error is recorded on the repository row.
Background catalog sync prefers the deployed release tag. If GitHub returns 404 for that tag, it falls back to recent releases and can import packages from those releases. Without a configured tag, it also scans recent releases. An interactive Plugins UI import stays on the selected tag: a missing release reports an error and imports nothing instead of substituting another release's catalog.
For both Wasm and native add-on background sync, a release that exists but lacks the required index asset does not trigger fallback. Missing catalogs (including a 404 from the fallback feed) and missing index assets do not trigger Oban retries: unless another repository has a retryable failure, the job completes and automatic sync tries again on its normal schedule (hourly by default). Other discovery failures, including HTTP 401, HTTP 5xx and invalid settings, still fail the job for retry. GitHub's private-repository 404 also follows the missing-catalog policy; check repository access when it occurs.
A completed job therefore does not prove that packages were imported. Inspect the Wasm repository's recorded sync error; permanent native add-on discovery failures are reported in error-level logs.
GitHub imports and verification
For GitHub-sourced plugins, the control plane fetches plugin.yaml, plugin.wasm, and an optional config schema. Commit verification is captured from GitHub. If PLUGIN_REQUIRE_GPG_FOR_GITHUB=true, unsigned or unverified commits are rejected during import.
Deployment and Storage Configuration
Wasm packages are served by the web-ng API and stored using a configurable backend. For production, store plugin blobs on persistent storage and back them up with normal platform operations. Plugin blob authorization is token-gated, with bearer tokens carried in request headers or POST bodies rather than embedded in request URLs.
Filesystem backend (default)
- Storage path:
/var/lib/serviceradar/plugin-packages - Configure web-ng with:
PLUGIN_STORAGE_BACKEND=filesystemPLUGIN_STORAGE_PATH=/var/lib/serviceradar/plugin-packagesPLUGIN_STORAGE_SIGNING_SECRET(shared with core for signed plugin blob tokens)
- Docker: mount a volume to
/var/lib/serviceradar/plugin-packagesin theweb-ngcontainer. - Kubernetes: mount a PVC at
/var/lib/serviceradar/plugin-packagesfor theweb-ngdeployment.
For core plugin blob delivery, set:
PLUGIN_STORAGE_PUBLIC_URL— base URL for web-ng (your deployment's web-ng endpoint)AGENT_PLUGIN_STORAGE_PUBLIC_URL— optional agent-facing override minted into agent download URLs ahead ofPLUGIN_STORAGE_PUBLIC_URL; use it when agents reach artifacts through a different address (for example the in-cluster gateway service when the load balancer has no hairpin NAT)PLUGIN_STORAGE_SIGNING_SECRET— must match web-ngPLUGIN_STORAGE_DOWNLOAD_TTL_SECONDS— default86400
Agents receive a plain plugin blob endpoint plus a separate short-lived token, so plugin config never contains a tokenized URL.
JetStream object store
To store plugin blobs in NATS JetStream instead, set:
PLUGIN_STORAGE_BACKEND=jetstreamPLUGIN_STORAGE_BUCKET=serviceradar_pluginsPLUGIN_STORAGE_JS_MAX_BUCKET_BYTES(default2147483648, 2 GiB)PLUGIN_STORAGE_JS_MAX_CHUNK_BYTESPLUGIN_STORAGE_JS_REPLICASPLUGIN_STORAGE_JS_STORAGE(fileormemory)PLUGIN_STORAGE_JS_TTL_SECONDS
This backend requires NATS JetStream to be available to web-ng.
The bucket is discard-new: once it reaches PLUGIN_STORAGE_JS_MAX_BUCKET_BYTES it refuses new uploads instead of evicting plugin blobs. An unset or blank value uses the default; a non-positive or non-integer value fails boot. web-ng applies the cap to an existing unlimited bucket on first use only when the stored bytes fit strictly below it. Otherwise the bucket's max_bytes is left unchanged and the configured, stored and current values are logged; reads and writes of the existing bucket keep working. Raise the value before the next start to converge such a bucket.
GitHub access and verification policy
GITHUB_TOKENorGH_TOKENfor private reposPLUGIN_REQUIRE_GPG_FOR_GITHUB=trueto reject unverified commitsPLUGIN_ALLOW_UNSIGNED_UPLOADS=falseto require signatures for uploads
AlienVault OTX Threat Intel
ServiceRadar ships a first-party alienvault-otx-threat-intel Wasm plugin for edge-side OTX collection. Use Settings -> Networks -> Threat Intel to assign the approved package to an agent, set the OTX base URL, page size, timeout, and a secret reference for the API key. The key is used by the plugin through the normal secret-ref flow and is not displayed back in the UI. The edge plugin needs outbound HTTPS egress to the configured OTX host, normally otx.alienvault.com:443.
The collector emits each accepted OTX page immediately as a separate plugin-result chunk. Submission waits for admission to the agent's bounded result queue before the plugin advances, while the gateway can forward admitted chunks as the Wasm invocation fetches later pages. The collector therefore does not assemble the full subscribed corpus in memory or silently drop a page under queue pressure. Core upserts each accepted page in transactional batches and advances the edge cursor only after persistence succeeds; a failed batch leaves the contiguous cursor safe for an idempotent retry.
Page size, pages per invocation, request timeout, retry attempts, the pull-wide attempt and wall-time budgets, and the host payload admission limit remain enforced. If a run reaches one of those bounds, core persists the continuation page and effective page size; the next scheduled invocation resumes there. Core-worker collection also queues the provider continuation instead of restarting at page one, then stores a completion high-water with a two-day overlap for the next root sync. Retrospective NetFlow matching walks the imported corpus with an internal UUID keyset batch and stores progress on the retrohunt run.
There is no separate Max IOCs completeness cap. Legacy max_iocs, max_indicators, and otx_max_indicators values are accepted and ignored. Assignment edits preserve unrelated configuration while removing the obsolete keys. The legacy database column remains inert for rollback compatibility during this release window and can be removed by a later cleanup migration after older supported releases no longer read it.
Core-hosted OTX sync polls from the control plane when Settings -> Networks -> Threat Intel has OTX enabled, Execution Mode set to Core Worker, and a Core OTX credential selected. Create or rotate an AlienVault OTX (core) API token in Settings -> Networks -> Credentials. The singleton feed supplies the scope; this core-owned descriptor does not provision device-query rules or edge assignments. The saved reference has a restrictive foreign key, appears in credential usage with a link back to Threat Intel settings, and prevents deletion while selected.
Upgrades move a previously saved OTX key into the canonical CNPG credential inventory without key re-entry. The migration copies encrypted scalar material inside CNPG and clears the legacy settings copy; it never prints or decrypts the key. The worker resolves its token through a persisted, audited broker grant restricted to GET requests to otx.alienvault.com:443. OTX API-key environment and file secrets are no longer consumed. The core endpoint is https://otx.alienvault.com; runtime settings can tune page size, timeout, retries, backoff, partition and the initial modified-since cursor without carrying authentication.
The NetFlow security scheduler keeps one sync chain on the configured interval, which is at least one hour. Edge Plugin mode leaves the core worker off. Use Sync Now and verify Sync Health shows a new successful run. Sync Now says the sync is already queued when a job is waiting. Sync Health shows the last attempt, last success, last failure, counts, and the modified_since cursor. A Stale badge means there has been no successful sync, or the last success is older than twice the longer of the configured interval and one day. A failed fetch records only the error kind and keeps the last success, counts, and cursor.
When raw payload archival is enabled in Threat Intel settings, core stores decoded OTX page payload snapshots in NATS Object Store. Archival is optional; if NATS Object Store is unavailable, normalized indicator ingest continues and the archive failure is logged. The core defaults are:
SERVICERADAR_OTX_RAW_BUCKET=serviceradar_threat_intelSERVICERADAR_OTX_RAW_TTL_SECONDS=0SERVICERADAR_OTX_RAW_MAX_BUCKET_BYTES(default1073741824, 1 GiB; same discard-new reconcile rule as the plugin bucket)SERVICERADAR_OTX_RAW_MAX_CHUNK_BYTESSERVICERADAR_OTX_RAW_REPLICAS=1SERVICERADAR_OTX_RAW_STORAGE=file
Operational Tips
- Keep per-agent engine limits conservative and override down in assignments if needed.
- Use the Settings -> Agent capacity view to confirm headroom before assignments.
- Store plugin source details in the manifest
sourcesection for auditability. - Review
signal_schemasfor plugins that emit events or logs; missing contracts force the UI back to generic JSON rendering. - Plugin result payloads should use canonical statuses
OK,WARNING,CRITICAL, orUNKNOWN. The agent maps common failure aliases (failed,fail,error) toCRITICALso a failed execution is visible as unhealthy.