Guide

Build with the Component SDK

Create collectors, tools, utilities, and remote adapters.

Prerequisites

  • Go 1.21+
  • epack CLI installed

Create a component

epack sdk new collector myservice

This creates a ready-to-build component project:

epack-collector-myservice/
├── main.go                           # Implementation
├── go.mod
├── README.md
├── .gitignore
├── docs/                             # Registry documentation
│   ├── overview.md
│   ├── configuration.md
│   └── examples.md
├── .slsa-goreleaser/                 # Multi-platform build configs
│   └── *.yml
└── .github/workflows/
    └── release.yaml                  # SLSA Level 3 + Sigstore signing

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 sample config and env vars
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 .

Implement a collector

Edit main.go to fetch from your API:

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.

Error types

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

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