A detector plugin turns project evidence into a Bomly dependency graph. Use a detector when Bomly needs a new way to read dependency data, such as a new package manager, a specialized manifest format, or an internal dependency source.
A detector is one sdk.Module with Kind: sdk.PluginKindDetector. The same module can be compiled into a host build (embedded) or served as a managed plugin binary with sdk.ServeModule — you write the component once. Plugin basics covers the module model, repository contract, configuration, testing, and release flow shared by every role; this guide covers what is specific to detectors.
The Bomly SDK API reference documents sdk.Module, sdk.DetectorModule, the sdk.Detector interface, sdk.DetectionRequest, sdk.DetectionResult, graph helpers, and the package-manager support types used below.
Start from the bomly-plugin-template repository ("Use this template" on GitHub). It ships a working matcher; turning it into a detector means changing the module kind, the descriptor, and the role method — the layout, manifest, tests, and release workflow stay the same:
plugin/ importable package: descriptor, Config, Detector, Module()
cmd/<binary-name>/
main.go one line: sdk.ServeModule(plugin.Module())
bomly-plugin.json package manifest ("kind": "detector")
testdata/ fixtures for unit tests
.github/workflows/ CI and the release workflow
go.mod pins a released github.com/bomly-dev/bomly-sdk version
The component lives in an importable plugin/ package that exports Module():
package plugin
import (
"context"
"fmt"
sdk "github.com/bomly-dev/bomly-sdk"
)
// Name must equal the "id" field in bomly-plugin.json.
const Name = "com.example.bun-lock-detector"
// Config is the detector's typed configuration block.
type Config struct {
IncludeDev bool `json:"includeDev" doc:"Include devDependencies in the graph" default:"false"`
}
// Detector reads bun.lock evidence and returns a dependency graph.
// sdk.BaseDetector supplies default Ready/Applicable implementations
// (always ready, always applicable).
type Detector struct {
sdk.BaseDetector
config Config
}
func descriptor() sdk.DetectorDescriptor {
return sdk.DetectorDescriptor{
Name: Name,
DisplayName: "Bun Lock Detector",
Aliases: []string{"bun-lock"},
Tags: []string{"dependency-detection", "bun"},
// Directories recursive discovery must never descend into for this
// ecosystem (see "Discovery metadata" below).
IgnoredDirectories: []string{"node_modules"},
ConfigSchema: sdk.MustConfigSchemaFor(Config{}),
}
}
func support() []sdk.PackageManagerSupport {
return []sdk.PackageManagerSupport{
sdk.Support(sdk.PackageManagerOther, "bun.lock", "bun.lockb", "package.json"),
}
}
func (d *Detector) Descriptor() sdk.DetectorDescriptor { return descriptor() }
func (d *Detector) PackageManagerSupport() []sdk.PackageManagerSupport { return support() }
// ResolveGraph reads the request and returns one or more manifest-scoped graphs.
func (d *Detector) ResolveGraph(ctx context.Context, req sdk.DetectionRequest) (sdk.DetectionResult, error) {
graph := sdk.New()
dep := sdk.NewDependency(sdk.Dependency{
Coordinates: sdk.Coordinates{
Ecosystem: sdk.EcosystemNPM,
Name: "is-odd",
Version: "3.0.1",
PURL: "pkg:npm/is-odd@3.0.1",
},
FoundBy: Name,
})
if err := graph.AddNode(dep); err != nil {
return sdk.DetectionResult{}, fmt.Errorf("add node: %w", err)
}
return sdk.DetectionResult{
SubprojectInfo: req.Subproject,
RootExecutionTarget: req.ExecutionTarget,
Graphs: sdk.SingleGraphContainer(graph, sdk.ManifestMetadata{
Path: "bun.lock",
}),
}, nil
}
// Module packages the detector for both execution modes.
func Module() sdk.Module {
return sdk.Module{
Kind: sdk.PluginKindDetector,
Detector: &sdk.DetectorModule{
Descriptor: descriptor(),
Support: support(),
New: func(_ context.Context, host sdk.HostContext) (sdk.Detector, error) {
detector := &Detector{}
if err := host.DecodeConfig(&detector.config); err != nil {
return nil, fmt.Errorf("decode %s config: %w", Name, err)
}
return detector, nil
},
},
}
}The binary entrypoint stays one line:
package main
import (
sdk "github.com/bomly-dev/bomly-sdk"
"example.com/bomly-plugin-bun-lock/plugin"
)
func main() { sdk.ServeModule(plugin.Module()) }Descriptoris the detector's static registration: name (must equal the manifestid), display name, aliases, tags, supported ecosystems and managers, discovery metadata, and config schema.Support(or thePackageManagerSupportmethod) tells Bomly which package managers and evidence files can plan this detector — declared on the module so Bomly can plan without constructing the component.Newconstructs the detector once per execution, with asdk.HostContextfor the logger, HTTP client, runtime info, and configuration.Ready(ctx, req) errorreports whether the detector can run right now. Returnnilwhen ready; return an error whose message explains the reason (for examplefmt.Errorf("bun executable not found on PATH")) when it cannot.sdk.BaseDetectorembeds an always-ready default.Applicable(ctx, req) (bool, error)reports whether the detector should run for this request (right project shape, right evidence present).ResolveGraphdoes the work: read evidence, build graphs, return them.
Honor the context in every method: probing, parsing, and subprocess work should stop promptly when the scan is cancelled.
Bomly plans detector chains per package manager and runs them first-success: when the planned primary detector reports not ready or not applicable, or fails, the next detector in the chain gets the request. Syft is typically the last fallback for ecosystems it covers.
Hand off gracefully instead of failing hard:
- Report a missing toolchain through
Readywith a clear reason, not through aResolveGrapherror. The reason appears in scan output when a fallback is used (FallbackReasonon the result the fallback produces). - Report "this project is not for me" through
Applicable, not through an empty graph. - Reserve
ResolveGrapherrors for real failures on projects the detector should have handled.
Detector plugins participate in subproject discovery and scan planning through descriptor fields, aggregated across every registered detector exactly like the built-ins:
PackageManagerSupport.EvidencePatterns— file names (such asbun.lock) whose presence plans this detector for a directory.DetectorDescriptor.IgnoredDirectories— directory basename globs recursive discovery (--recursive) must not descend into (a Node detector declaresnode_modules, a Maven detector declarestarget).DetectorDescriptor.IgnoredDirectoryMarkers— file names that mark a directory as ignored regardless of its name (pyvenv.cfgmarks a Python virtualenv).sdk.Support(...).WithMultiModule()— declares that the detector natively expands nested workspace or reactor modules from a root manifest, so recursive discovery does not scan the same modules twice.
All of these are optional; older plugins that omit them keep working.
Use SDK graph helpers instead of constructing graph internals by hand:
parent := sdk.NewDependency(sdk.Dependency{
Coordinates: sdk.Coordinates{Name: "app", Version: "0.0.0", PURL: "pkg:generic/app@0.0.0"},
})
child := sdk.NewDependency(sdk.Dependency{
Coordinates: sdk.Coordinates{Name: "lodash", Version: "4.17.21", PURL: "pkg:npm/lodash@4.17.21"},
})
graph := sdk.New()
if err := graph.AddNode(parent); err != nil {
return sdk.DetectionResult{}, err
}
if err := graph.AddNode(child); err != nil {
return sdk.DetectionResult{}, err
}
if err := graph.AddEdge(parent.ID, child.ID); err != nil {
return sdk.DetectionResult{}, err
}Prefer canonical PURLs and fill Coordinates where possible — matchers enrich packages by PURL, and findings reference them the same way. Return req.Subproject and req.ExecutionTarget in the result so Bomly keeps it tied to the planned scan target. Use DetectionResult.Warnings for non-fatal problems worth surfacing (the graph is usable, but something about the project will degrade an install elsewhere).
Install-first. A detector that must prepare dependencies before reading them (for example, running a resolving install) implements sdk.InstallFirstDetector (Install(ctx, req) error) and sets SupportsInstallFirst in its descriptor. Do not install package managers themselves; Bomly assumes required package managers already exist.
Remediation hints. A detector that understands package-manager fix strategies can advertise them and contribute read-only evidence after vulnerability enrichment:
// In the descriptor:
RemediationCapabilities: []sdk.RemediationCapability{{
SupportedManagers: []sdk.PackageManager{sdk.PackageManagerNPM},
Actions: []sdk.RemediationAction{
sdk.RemediationActionDirectBump,
sdk.RemediationActionTransitiveOverride,
},
}},Then implement sdk.DetectorRemediationProvider on the detector:
func (d *Detector) RemediationHints(ctx context.Context, req sdk.RemediationHintRequest) (sdk.RemediationHintResponse, error)Return hints only for dependency IDs and manifest paths this detector produced. Hints may name supported strategies and give plain-language package-manager advice. They must not choose a fix version, edit files, run commands, or make network calls — Bomly validates every hint and the central remediation component chooses the final action. Detectors without the capability are simply never asked.
Declare a typed Config struct with json, doc:, and default: tags, advertise it with ConfigSchema: sdk.MustConfigSchemaFor(Config{}), and decode it in New with host.DecodeConfig(&cfg). Users set the block under plugins.detectors.<name>:
plugins:
detectors:
com.example.bun-lock-detector:
includeDev: trueThe same block reaches the component in both execution modes. See Configuration in the plugin guide for details and the deprecated flat form.
Unit-test parsing and graph construction against testdata/ fixtures, and run the SDK conformance suite:
func TestConformance(t *testing.T) {
conformance.Test(t, conformance.Config{
Module: Module(),
ManifestPath: filepath.Join("..", "bomly-plugin.json"),
})
}Local development loop:
go build -o ./bin/bomly-plugin-bun-lock ./cmd/bomly-plugin-bun-lock
bomly plugins install ./bin/bomly-plugin-bun-lock --dev
bomly plugins enable com.example.bun-lock-detector
bomly scan --path ./my-project --detectors com.example.bun-lock-detector --json
bomly plugins verify com.example.bun-lock-detector
bomly plugins test com.example.bun-lock-detector
bomly plugins doctor com.example.bun-lock-detectorUse --detectors +<name> to add the detector to the default set instead of replacing it. See Testing a plugin for the shared workflow, including conformance.ProbeBinary for probing the built binary over the real managed transport.
Follow the template's release workflow: one archive per platform named <name>_<version>_<os>_<arch>.tar.gz (.zip on Windows) containing the binary, bomly-plugin.json, README.md, and LICENSE, plus a SHA256SUMS file. The manifest's entrypoint map names the binary per platform, and descriptor.Name must equal the manifest id. See Package and release.
- Declare accurate package-manager support, evidence patterns, and discovery metadata.
- Report missing toolchains through
Readywith a clear reason; hand off to the chain instead of failing. - Honor context cancellation in probing, parsing, and subprocesses.
- Prefer canonical PURLs and filled
Coordinates. - Keep remediation hints read-only and within the advertised capabilities.
- Wrap errors with useful context; avoid panics.
- Do not log secrets, tokens, or credentials.
- Keep network calls explicit and document them in the plugin README.
- Add unit tests for parsing and graph construction, plus
conformance.Test.