Custom security graph collectors
Built-in collectors cover AWS, Azure, GCP, Kubernetes, and the demo graph. Anything else enters through an external executable. In-process Go plugins are not supported. The contract is one JSON object on stdout, so the collector can be written in any language.
Plugins are trusted code. They run as the user who invokes om, inherit that
environment and working directory, and should stay read-only toward the systems they
inventory. om does not sandbox the process.
Run
go build -o example-collector ./examples/collector
./om scan plugin -- ./example-collector
./om scan plugin --timeout 5m -- ./my-collector --region us-east-1 --timeout defaults to 10 minutes and cancels the process. Arguments after
-- belong to the plugin. Flags before -- belong to om.
Exit 0 means the batch was produced. om validates it, then upserts. A non-zero
exit fails the scan and includes stderr when the plugin wrote any.
- Stdout is capped at 32 MiB. Larger output is rejected and the process is killed.
- Stderr is capped at 64 KiB and is the place for logs. Do not print the batch there.
- Stdout must be exactly one JSON object. A second value, or log lines mixed into stdout, fails decode.
A successful plugin scan prints the same line as a cloud scan:
Ingested N nodes and M edges from plugin <basename>.
It does not run CSPM rules. Follow it with om rules run.
Stdout document
Field names match sdk/collector. Schema version is the constant
SchemaVersion = 1. The JSON itself has no version field.
{
"nodes": [
{
"id": "internet:global",
"type": "Internet",
"name": "Internet",
"provider": "plugin"
},
{
"id": "plugin:host:edge-1",
"type": "Workload",
"name": "edge-1",
"provider": "plugin",
"account_id": "lab",
"properties": { "public_ip": true }
},
{
"id": "plugin:bucket:logs",
"type": "Datastore",
"name": "logs",
"provider": "plugin",
"account_id": "lab",
"properties": { "public_access": true, "service": "s3" }
}
],
"edges": [
{
"source_id": "internet:global",
"target_id": "plugin:host:edge-1",
"type": "REACHABLE"
},
{
"source_id": "plugin:host:edge-1",
"target_id": "plugin:bucket:logs",
"type": "CAN_ACCESS"
}
]
} Validation, after empty edge ids are filled:
- Node
id,type, andnameare required. Duplicate node ids fail. - Types must be
Internet,Network,Workload,Identity,Datastore,Finding, orControl. - Edge types must be
REACHABLE,ASSUMES,CAN_ACCESS,AFFECTS, orVIOLATES. - An empty edge id becomes
source|target|type. Duplicate edge ids fail. - Both endpoints must be nodes in the same batch. An edge to a node already in Postgres, but missing from this document, is rejected.
POST /v1/ingest does not run this validation. Prefer om scan plugin
for external collectors. Include internet:global when the batch should show up
in attack path queries. Set public_access,
admin_access, and the other keys on
Schema when you want pack rules to match. On a datastore, set
sensitivity to a non-empty string to mark a crown jewel.
internet-to-sensitive-datastore keeps only those paths. Built-in collectors copy
that value from a tag or label named sensitivity or data-class; a
plugin sets the property itself. On a workload,
packages, image, and images are what
om enrich cve matches. A package entry is a CPE 2.3 name or a versioned package
URL. See CVE enrichment.
Go SDK
Implement collector.Collector (Collect(ctx) (Batch, error)) and call
collector.Run from main. Run writes the batch to stdout
and exits non-zero on error. examples/collector is the reference: an
internet-reachable host with a package URL, an image ref, and CAN_ACCESS to a
public datastore. Use
collector.InternetNodeID (internet:global) and the type constants
rather than string literals.
Upserts keep the last batch’s properties for a given id. A plugin that omits
internet:global on a later run does not delete the internet node another scan
wrote. Resources you stop emitting stay in the graph. Delete them yourself, or scope them
with account_id and remove that account the way om scan demo does
for its sample accounts.
The design record is ADR 004.
Copyright © 2026 OpenSourceOM. Licensed under Apache-2.0.