Guide

Build with the Component SDK

Create collectors, tools, utilities, and remote adapters.

Prerequisites

  • Go 1.26+
  • epack CLI installed

Create a component

epack sdk new collector myservice

This creates a ready-to-build component project. Pass --module to set its Go module path:

epack-collector-myservice/
├── cmd/epack-collector-myservice/
│   └── main.go                       # Entrypoint: config, secrets, exit codes
├── internal/collector/
│   ├── collector.go                  # Collection logic
│   └── collector_test.go
├── testdata/conformance.json         # Config the conformance harness collects with
├── Makefile                          # build, test, lint, conformance
├── go.mod
├── README.md
├── .gitignore
├── docs/                             # Registry documentation
│   ├── overview.md
│   ├── configuration.md
│   └── examples.md
├── .slsa-goreleaser/                 # Multi-platform build configs
│   └── *.yml
└── .github/workflows/
    ├── ci.yaml                       # Tests, lint, conformance
    └── release.yaml                  # SLSA Level 3 + Sigstore signing

A collector's collection logic in internal/collector does not import the SDK, so its tests run without a collector process. Tools, utilities, and remotes keep their implementation in main.go.

Component types

epack sdk new collector myservice    # Gathers evidence from APIs
epack sdk new tool analyzer          # Processes packs
epack sdk new utility viewer         # Standalone CLI tool
epack sdk new remote mycloud         # Push/pull adapter

Develop with watch mode

Run your component with automatic rebuild on file changes:

cd epack-collector-myservice
epack sdk run --watch .

Pass arguments after --:

epack sdk run --watch . -- --capabilities

Generate test data

Create sample inputs for testing:

# Generate test fixtures
epack sdk mock tool              # Creates sample-evidence.epack
epack sdk mock collector         # Creates config.json and run-collector.sh
epack sdk mock tool -o testdata  # Output to testdata/

Run conformance tests

Verify your component implements the protocol correctly:

# Build and test
epack sdk test .

# Test a binary directly
epack sdk test ./epack-collector-myservice

# Verbose output
epack sdk test --verbose .

# Collect with a config, and check how the collector handles its credential
epack sdk test --config testdata/conformance.json --secret MYSERVICE_API_TOKEN .

# What CI runs in a collector project
make conformance

epack sdk test builds cmd/<directory name>, or the only package under cmd/, and passes a collector project's testdata/conformance.json unless --config names another. With a config, the output, signal, and timeout checks run against a real collection. Each --secret is set to a canary value on every run: the checks fail when the canary shows up in output, files, or error messages, and when the collector reports a rejected credential without exiting 3. make conformance takes the secret names from CONFORMANCE_SECRETS in the Makefile.

Implement a collector

The entrypoint's run function reads config and secrets, calls your API, and emits artifacts:

cmd/epack-collector-myservice/main.go
func run(ctx componentsdk.CollectorContext) error {
    // Get config from epack.yaml
    org := ctx.Config()["organization"].(string)

    // Get secrets from environment
    token := ctx.Secret("MYSERVICE_API_TOKEN")

    // Call your API
    data, err := fetchFromAPI(org, token)
    if err != nil {
        return componentsdk.NewNetworkError("API call failed: %v", err)
    }

    // Emit collected artifacts
    return ctx.Emit([]componentsdk.CollectedArtifact{
        componentsdk.JSONArtifact("artifacts/myservice.json", map[string]any{
            "collected_at": time.Now().UTC().Format(time.RFC3339),
            "organization": org,
            "mfa_enabled":   data.MFAEnabled,
            "user_count":    data.UserCount,
        }, componentsdk.ArtifactMeta{
            DisplayName: "MyService posture",
            Description: "MFA and user posture from MyService",
            Controls:    []string{"SOC2-CC6.1"},
        }),
    })
}

Artifact metadata is recorded in the pack manifest. Use DisplayName and Description for readable epack inspect and epack list output, and Controls to tag artifacts with the controls they support.

A collector that exits 0 must emit at least one artifact. Empty output, JSON null, or text that is not JSON fails the collector with invalid_output.

Error types

componentsdk.NewConfigError("missing field: %s", field)   # Exit 2
componentsdk.NewAuthError("invalid token")                 # Exit 3
componentsdk.NewNetworkError("request failed: %v", err)    # Exit 4

A typed error keeps its exit code when wrapped with %w. An error that reaches context.DeadlineExceeded without one exits 4, and any other error exits 1. epack reports these as config_invalid, auth_failed, network_error, and collector_error; see collector run failures.

Collection levels

Collectors can gate optional fields on the caller's requested level. Read it via ctx.Level() and compare with AtLeast. Absent, empty, or unknown values downgrade to LevelTrust with a warning on stderr, so collectors never silently gather more than was asked for.

level := ctx.Level()

if level.AtLeast(componentsdk.LevelAudit) {
    // gather per-resource detail an auditor would want
}
if level.AtLeast(componentsdk.LevelInternal) {
    // gather raw inventory (user lists, audit-log slices, etc.)
}

Each collector documents which of its fields appear at audit vs internal. Stamp collected_at_level on the top-level artifact so downstream tools can interpret missing fields correctly.

File artifacts

Use file artifacts when a collector needs to package raw bytes such as PDFs, screenshots, or archives. The runner provides a per-collector staging directory; SDK collectors access it through componentsdk.Staging(ctx).

staging, ok := componentsdk.Staging(ctx)
if !ok || staging.OutputDir() == "" {
    return componentsdk.NewConfigError("file artifacts require epack v0.2.0 or newer")
}

name, err := staging.WriteStagedFile("documents/policy.pdf", pdfBytes)
if err != nil {
    return err
}

return ctx.Emit([]componentsdk.CollectedArtifact{
    componentsdk.FileArtifact("artifacts/documents/policy.pdf", name,
        componentsdk.ArtifactMeta{DisplayName: "Security policy"}),
})

For streaming output, use StageFile, close the returned handle, and emit the returned name. File artifacts require an explicit pack Path and are mutually exclusive with Data.

Implement a tool

main.go
func run(ctx componentsdk.ToolContext) error {
    // Access the pack
    pack := ctx.Pack()

    // Read an artifact
    data, err := pack.ReadArtifact("artifacts/github-repos.json")
    if err != nil {
        return err
    }

    // Analyze
    result := analyze(data)

    // Write output
    return ctx.WriteOutput("analysis.json", result)
}

Pack access

pack.Manifest()                                    # Get manifest
pack.ReadArtifact("artifacts/report.json")         # Read artifact
ctx.Warn("CODE", "message", "path")                  # Add warning
ctx.Error("CODE", "message", "path")                 # Add error

Implement a utility

main.go
func run(args []string) error {
    if len(args) == 0 {
        return fmt.Errorf("no pack file specified")
    }

    pack, err := componentsdk.OpenPack(args[0])
    if err != nil {
        return err
    }
    defer pack.Close()

    fmt.Printf("Stream: %s\n", pack.Manifest().Stream)
    return nil
}

Implement a remote adapter

main.go
type handler struct{}

func (h *handler) PushPrepare(req componentsdk.PushPrepareRequest) (*componentsdk.PushPrepareResponse, error) {
    url, token := generateUploadURL(req.Target.Workspace, req.Pack.Digest)
    return &componentsdk.PushPrepareResponse{
        Upload:        componentsdk.UploadInfo{Method: "PUT", URL: url},
        FinalizeToken: token,
    }, nil
}

func (h *handler) PushFinalize(req componentsdk.PushFinalizeRequest) (*componentsdk.PushFinalizeResponse, error) {
    release := createRelease(req.FinalizeToken)
    return &componentsdk.PushFinalizeResponse{
        Release: componentsdk.ReleaseResult{ReleaseID: release.ID, PackDigest: release.Digest},
    }, nil
}

func (h *handler) PullPrepare(req componentsdk.PullPrepareRequest) (*componentsdk.PullPrepareResponse, error) {
    url, digest, token := getDownloadURL(req.Target.Workspace, req.Ref)
    return &componentsdk.PullPrepareResponse{
        Download:      componentsdk.DownloadInfo{URL: url},
        Pack:          componentsdk.PackResult{Digest: digest},
        FinalizeToken: token,
    }, nil
}

func (h *handler) PullFinalize(req componentsdk.PullFinalizeRequest) (*componentsdk.PullFinalizeResponse, error) {
    return &componentsdk.PullFinalizeResponse{Confirmed: true}, nil
}

Release

Tag a release to trigger the scaffolded GitHub workflow:

git tag v0.2.0
git push origin v0.2.0

The workflow builds for linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 with SLSA Level 3 provenance and Sigstore signing.

Configure in epack.yaml

Collectors and tools

epack.yaml
collectors:
  myservice:
    source: ghcr.io/yourorg/epack-collector-myservice@v1
    config:
      organization: acme
    secrets:
      - MYSERVICE_API_TOKEN

tools:
  analyzer:
    source: ghcr.io/yourorg/epack-tool-analyzer@v1

Remotes

epack.yaml
remotes:
  mycloud:
    source: ghcr.io/yourorg/epack-remote-mycloud@v1
    target:
      workspace: acme
    auth:
      mode: api_key

Utilities

Utilities are standalone CLI tools invoked directly:

epack utility viewer evidence.epack