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:
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
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
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
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
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
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
Related
- SDK Reference — Full API documentation
- Component Protocol — Wire format
- Run collectors — Using collectors