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:
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
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