plugin

package
v0.0.0-...-1a28f28 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 22, 2026 License: MIT Imports: 11 Imported by: 0

README

Deputy Plugin SDK

Build custom inventory extractors for Deputy in any language.

Overview

Deputy supports three types of inventory extractors:

  1. OSV-SCALIBR - Built-in extractors from Google's osv-scalibr project
  2. Deputy Built-in - Custom Go extractors in internal/inventory/plugins/
  3. Plugins - External executables via pluginrpc (this SDK)

Plugins are standalone executables that Deputy invokes via subprocess using pluginrpc. This enables:

  • Any language - Write plugins in Go, Rust, Python, or any language
  • Isolation - Plugins run in separate processes
  • Distribution - Ship plugins as standalone binaries
  • Hot reload - Update plugins without recompiling Deputy

Quick Start (Go)

package main

import "github.com/picatz/deputy/sdk/plugin"

func main() {
    plugin.Main(&myExtractor{})
}

type myExtractor struct{}

func (e *myExtractor) Name() string        { return "custom/myformat" }
func (e *myExtractor) DisplayName() string { return "My Format Extractor" }
func (e *myExtractor) Ecosystem() string   { return "custom" }
func (e *myExtractor) Version() int        { return 1 }
func (e *myExtractor) Description() string { return "Extracts packages from .myformat files" }
func (e *myExtractor) FilePatterns() []string { return []string{"*.myformat"} }

func (e *myExtractor) FileRequired(path string, isDir bool, mode uint32, size int64) bool {
    return strings.HasSuffix(path, ".myformat")
}

func (e *myExtractor) Extract(path string, contents []byte, root string) ([]*plugin.Package, error) {
    // Parse contents and return packages
    return []*plugin.Package{
        plugin.NewPackage("example-pkg", "1.0.0", "custom"),
    }, nil
}

Build and install:

go build -o deputy-extractor-myformat .

Plugin Interface

Implement the Extractor interface:

type Extractor interface {
    // Metadata
    Name() string           // Unique identifier (e.g., "ruby/gemspec")
    DisplayName() string    // Human-readable name
    Ecosystem() string      // Package ecosystem (go, npm, pypi, etc.)
    Version() int           // Increment on behavior changes
    Description() string    // What does this extractor do?
    FilePatterns() []string // Glob patterns for matched files

    // File filtering (called for every file - keep fast!)
    FileRequired(path string, isDir bool, mode uint32, size int64) bool

    // Package extraction (called only for required files)
    Extract(path string, contents []byte, root string) ([]*Package, error)
}

Creating Packages

Use the builder pattern for complex packages:

pkg := plugin.NewPackageBuilder("lodash", "4.17.21", "npm").
    WithPURL("pkg:npm/lodash@4.17.21").
    WithLicenses("MIT").
    WithDirect(true).
    WithLocations("package.json").
    WithManifestRef("package.json", "npm", "dependencies").
    Build()

Or the simple constructor:

pkg := plugin.NewPackage("lodash", "4.17.21", "npm")

Registration

Via Configuration
# .deputy.yaml
plugins:
  extractors:
    - path: /usr/local/bin/deputy-extractor-myformat
    - name: deputy-extractor-gemspec  # searches PATH
Via PATH Discovery

Plugins named deputy-extractor-* in PATH are auto-discovered.

Protocol

Plugins communicate via pluginrpc:

  • Requests are sent via stdin (protobuf or JSON)
  • Responses return via stdout
  • Errors use gRPC-style status codes
  • No network required - just subprocess invocation
Testing Your Plugin
# Protocol version
./my-plugin --protocol

# Available procedures
./my-plugin --spec

# Get metadata
./my-plugin info --format json

# Test file matching (binary format)
echo '<protobuf-request>' | ./my-plugin file-required

# Extract packages (binary format)
echo '<protobuf-request>' | ./my-plugin extract

Distributed Tracing

Plugins automatically participate in distributed traces when OpenTelemetry is configured:

  1. Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable
  2. Deputy injects W3C TraceContext into requests
  3. Plugin SDK extracts context and creates child spans
  4. Traces flow seamlessly across process boundaries
Deputy Scan (parent span)
└── plugin.client.FileRequired (child span)
    └── plugin.FileRequired (child span in plugin process)
└── plugin.client.Extract (child span)
    └── plugin.Extract (child span in plugin process)

Writing Plugins in Other Languages

Implement the ExtractorService from api/deputy/plugin/v1/extractor.proto:

service ExtractorService {
  rpc Info(InfoRequest) returns (InfoResponse);
  rpc FileRequired(FileRequiredRequest) returns (FileRequiredResponse);
  rpc Extract(ExtractRequest) returns (ExtractResponse);
}

Your plugin must:

  1. Respond to --protocol with 1
  2. Respond to --spec with the procedure spec
  3. Handle info, file-required, and extract subcommands
  4. Read protobuf requests from stdin
  5. Write protobuf responses to stdout

See pluginrpc for protocol details.

Examples

API Reference

See pkg.go.dev for full documentation.

Documentation

Overview

Package plugin provides a simple SDK for building Deputy extractor plugins.

Plugins are standalone executables that Deputy invokes via pluginrpc to extract package inventory from custom file formats. This SDK provides a high-level interface that handles all the pluginrpc boilerplate.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     Deputy Process                              │
│  ┌───────────┐    ┌──────────────┐    ┌───────────────────┐    │
│  │   scan    │───▶│  inventory   │───▶│  plugin.Client    │    │
│  │  command  │    │  extraction  │    │  (invokes plugin) │    │
│  └───────────┘    └──────────────┘    └─────────┬─────────┘    │
│                                                 │              │
└─────────────────────────────────────────────────│──────────────┘
                                                  │
                          ┌────────────────┬──────┴───────┐
                          │ stdin (proto)  │  spawn       │
                          ▼                ▼              │
┌─────────────────────────────────────────────────────────│──────┐
│                     Plugin Process                      │      │
│  ┌───────────────────┐    ┌───────────────┐    ┌───────┴────┐ │
│  │ pluginrpc.Server  │◀───│   Extractor   │◀───│  sdk/plugin│ │
│  │ (protocol logic)  │    │ (your impl)   │    │  (helpers) │ │
│  └─────────┬─────────┘    └───────────────┘    └────────────┘ │
│            │                                                   │
│            ▼ stdout (proto)                                    │
└────────────│───────────────────────────────────────────────────┘
             │
             └────▶ back to Deputy

Quick Start

Create a plugin in a single main.go file:

package main

import (
    "github.com/picatz/deputy/sdk/plugin"
)

func main() {
    plugin.Main(&myExtractor{})
}

type myExtractor struct{}

func (e *myExtractor) Name() string        { return "custom/myformat" }
func (e *myExtractor) DisplayName() string { return "My Custom Format" }
func (e *myExtractor) Ecosystem() string   { return "custom" }
func (e *myExtractor) Version() int        { return 1 }
func (e *myExtractor) Description() string { return "Extracts packages from .myformat files" }
func (e *myExtractor) FilePatterns() []string { return []string{"*.myformat"} }

func (e *myExtractor) FileRequired(path string, isDir bool, mode uint32, size int64) bool {
    return strings.HasSuffix(path, ".myformat")
}

func (e *myExtractor) Extract(path string, contents []byte, root string) ([]*plugin.Package, error) {
    // Parse contents and return packages
    return []*plugin.Package{
        {Name: "example-pkg", Version: "1.0.0", Ecosystem: "custom"},
    }, nil
}

Building the Plugin

go build -o deputy-extractor-myformat ./cmd/myformat-plugin

Registering with Deputy

Plugins can be registered via configuration or discovered from PATH:

# .deputy.yaml
plugins:
  extractors:
    - path: /usr/local/bin/deputy-extractor-myformat
    - name: deputy-extractor-gemspec  # searches PATH

Or programmatically via the SDK:

client, _ := sdk.NewClient(ctx)
client.RegisterExtractor(ctx, "deputy-extractor-myformat")

Distributed Tracing

Plugins automatically participate in distributed traces when the OTEL_EXPORTER_OTLP_ENDPOINT environment variable is set. The SDK extracts trace context from requests and creates child spans.

Deputy Scan (trace-id: abc123)
│
├── inventory.Extract
│   │
│   ├── plugin.client.FileRequired ──────────────────┐
│   │   │                                            │ TraceContext
│   │   └──[spawn]──▶ plugin.FileRequired (abc123) ◀─┘ in request
│   │
│   └── plugin.client.Extract ───────────────────────┐
│       │                                            │ TraceContext
│       └──[spawn]──▶ plugin.Extract (abc123) ◀──────┘ in request
│
└── vulnerability.Lookup

The TraceContext field in FileRequiredRequest and ExtractRequest carries the W3C traceparent header value, enabling end-to-end tracing.

Plugin Interface

Implement the Extractor interface to create a plugin. The SDK handles:

  • Pluginrpc protocol negotiation (--protocol, --spec flags)
  • Request/response serialization
  • OpenTelemetry trace context propagation
  • Error handling and exit codes

See the Extractor interface for detailed documentation.

Distributed tracing support for Deputy extractor plugins.

This file provides OpenTelemetry trace context propagation, enabling plugins to participate in distributed traces with Deputy. When OTEL_EXPORTER_OTLP_ENDPOINT is set, traces flow seamlessly across process boundaries.

Trace Context Flow

Deputy Process                              Plugin Process
+-----------------------+                   +------------------------+
|                       |                   |                        |
|  ctx with span        |                   |  extractTraceContext() |
|       |               |                   |       |                |
|       v               |                   |       v                |
|  injectTraceContext() |                   |  ctx with linked span  |
|       |               |                   |       |                |
|       v               |   TraceContext    |       v                |
|  "00-abc-def-01" -----|---- field ------->|  startSpan()           |
|                       |   in request      |       |                |
|                       |                   |       v                |
|                       |                   |  child span created    |
+-----------------------+                   +------------------------+

The TraceContext field in FileRequiredRequest and ExtractRequest carries the W3C traceparent header value (e.g., "00-traceid-spanid-01").

Index

Constants

View Source
const (
	// TracerName is the tracer name for plugin spans.
	// All spans created by plugin SDK use this tracer for identification.
	TracerName = "github.com/picatz/deputy/plugin"
)

Variables

This section is empty.

Functions

func Main

func Main(extractor Extractor)

Main is the entry point for extractor plugins. Call this from your main() function with your Extractor implementation.

Example:

func main() {
    plugin.Main(&myExtractor{})
}

Types

type Extractor

type Extractor interface {
	// Name returns the extractor identifier.
	// Convention: "<ecosystem>/<format>" (e.g., "ruby/gemspec", "custom/myformat").
	Name() string

	// DisplayName returns a human-readable name for UI display.
	DisplayName() string

	// Ecosystem returns the package ecosystem this extractor supports.
	// Use standard ecosystem names: go, npm, pypi, maven, rubygems, cargo, etc.
	Ecosystem() string

	// Version returns the extractor version (increment on behavior changes).
	Version() int

	// Description returns context about what this extractor does.
	Description() string

	// FilePatterns returns glob patterns for files this extractor handles.
	// Examples: ["Gemfile.lock", "*.gemspec", "vendor/*/Gemfile"]
	FilePatterns() []string

	// FileRequired checks if this extractor should process a file.
	// Return true to receive an Extract call for this file.
	// This is called for every file in the scan - keep it fast (no I/O).
	//
	// Parameters:
	//   - path: File path relative to scan root
	//   - isDir: Whether the path is a directory
	//   - mode: Unix file permission mode
	//   - size: File size in bytes
	FileRequired(path string, isDir bool, mode uint32, size int64) bool

	// Extract extracts packages from a file's contents.
	// Called only for files where FileRequired returned true.
	//
	// Parameters:
	//   - path: File path relative to scan root
	//   - contents: Raw file bytes
	//   - root: Absolute path to scan root (for resolving relative paths)
	//
	// Returns a slice of discovered packages and any error.
	Extract(path string, contents []byte, root string) ([]*Package, error)
}

Extractor is the interface that plugins must implement.

The interface is designed to be simple while matching the semantics of OSV-SCALIBR extractors for consistency across the ecosystem.

type ExtractorInfo

type ExtractorInfo = pluginv1.ExtractorInfo

ExtractorInfo is an alias for the proto ExtractorInfo type.

type Package

type Package = dependencyv1.Package

Package is an alias for the proto Package type for convenience. This allows plugin authors to use plugin.Package instead of importing the dependency proto package directly.

func NewPackage

func NewPackage(name, version, ecosystem string) *Package

NewPackage creates a new Package with the given name, version, and ecosystem. This is a convenience function for plugin authors.

Example:

pkg := plugin.NewPackage("lodash", "4.17.21", "npm")

type PackageBuilder

type PackageBuilder struct {
	// contains filtered or unexported fields
}

PackageBuilder provides a fluent interface for building Package instances.

Example:

pkg := plugin.NewPackageBuilder("lodash", "4.17.21", "npm").
    WithPURL("pkg:npm/lodash@4.17.21").
    WithLicenses("MIT").
    WithDirect(true).
    Build()

func NewPackageBuilder

func NewPackageBuilder(name, version, ecosystem string) *PackageBuilder

NewPackageBuilder creates a new PackageBuilder with required fields.

func (*PackageBuilder) Build

func (b *PackageBuilder) Build() *Package

Build returns the constructed Package.

func (*PackageBuilder) WithDirect

func (b *PackageBuilder) WithDirect(direct bool) *PackageBuilder

WithDirect marks the package as a direct dependency.

func (*PackageBuilder) WithLicenses

func (b *PackageBuilder) WithLicenses(licenses ...string) *PackageBuilder

WithLicenses sets the SPDX license identifiers.

func (*PackageBuilder) WithLocations

func (b *PackageBuilder) WithLocations(locations ...string) *PackageBuilder

WithLocations sets the file paths where this package was found.

func (*PackageBuilder) WithManifestRef

func (b *PackageBuilder) WithManifestRef(path, manager string, groups ...string) *PackageBuilder

WithManifestRef adds a manifest reference describing where the dependency is declared.

func (*PackageBuilder) WithPURL

func (b *PackageBuilder) WithPURL(purl string) *PackageBuilder

WithPURL sets the Package URL (PURL).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL