Go SBOM Generation Guide
Quick Answer
For Go projects, generate an SBOM fromgo.mod and go.sum for source dependency visibility, then scan the built binary or container image when you need release-accurate inventory. Validate the output with the SBOM Validator, especially if the file will be used for customer delivery, vulnerability review, or compliance evidence.
Introduction: Go's Unique Dependency Landscape
Go is one of the most interesting ecosystems for SBOM generation because the source dependency graph can be very clear while the final deployed binary is highly optimized and statically linked. That makes Go a strong candidate for SBOM automation, but it also means teams need to be precise about whether they are documenting source dependencies, build-time dependencies, or the contents of the shipped artifact.
This guide focuses on the practical side of Go SBOM generation:go.mod and go.sum, binary and container analysis, the tradeoffs between native Go metadata and general-purpose tools such as Syft, and how to validate the output with the SBOM Validator.
Generating accurate SBOMs for Go applications is crucial for security and compliance, especially given Go's popularity in cloud-native infrastructure. From Kubernetes to Docker to Terraform, many critical infrastructure tools are written in Go, making supply chain security paramount. This comprehensive guide covers all the tools and techniques for generating high-quality SBOMs for Go applications, addressing the unique aspects of the Go ecosystem.
Why SBOMs Matter for Go Applications
Understanding Go's Dependency Model
Go's approach to dependencies differs significantly from other languages, creating unique considerations for SBOM generation. The language's philosophy of simplicity extends to dependency management, but this simplicity masks sophisticated underlying mechanisms that affect how we track and secure dependencies.
Go applications typically include several categories of dependencies, each requiring different handling for accurate SBOM generation:
- Direct dependencies: Modules explicitly listed in
go.mod, representing packages your code directly imports. These are version-pinned and cryptographically verified throughgo.sum. - Transitive dependencies: Dependencies of your dependencies, automatically resolved by Go modules. While not directly imported, these are equally important for security as they're compiled into your binary.
- Standard library: Go's extensive standard library, which is versioned with the Go compiler itself. While generally secure, standard library vulnerabilities do occur and must be tracked.
- CGO dependencies: C libraries linked via CGO, which bypass Go's module system entirely. These require special attention as they're not captured in
go.modand can introduce platform-specific vulnerabilities. - Vendor dependencies: Vendored modules (if using
go mod vendor) create a local copy of dependencies. While this ensures availability, it can mask dependency updates and security patches. - Build-time dependencies: Tools used during compilation, including code generators and linters. While not in the final binary, these can introduce supply chain risks.
Go's static linking means all these dependencies are embedded in the final binary, making it crucial to track what's included. Unlike dynamically linked languages where you can update a library without recompiling, Go applications must be rebuilt to incorporate security patches, making accurate SBOMs essential for vulnerability management.
Go Modules and Dependency Management
The Foundation: go.mod and go.sum Files
Go modules, introduced in Go 1.11 and refined through subsequent releases, revolutionized Go dependency management. Thego.mod file serves as the manifest declaring your module's requirements, while go.sum provides cryptographic checksums ensuring reproducible builds. Understanding these files is fundamental to generating accurate SBOMs.
The go.mod file is more than a simple dependency list - it's a semantic versioning-aware manifest that captures the minimum version requirements for your application. Go's Minimal Version Selection (MVS) algorithm uses this information to build a consistent dependency graph, ensuring reproducible builds across different environments.
Understanding go.mod and go.sum
// go.mod example
module github.com/mycompany/myapp
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/jackc/pgx/v5 v5.4.3
gorm.io/gorm v1.25.2
)
require (
// Indirect dependencies
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
// ... more indirect dependencies
)go.sum file contains cryptographic hashes:
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=SBOM Generation Tools
Choosing the Right Tool for Go SBOM Generation
The Go ecosystem offers several tools for SBOM generation, each with different strengths. Syft has emerged as the de facto standard due to its deep understanding of Go's build system and ability to analyze both source code and compiled binaries. However, other tools like CycloneDX's Go module support and native Go tooling also play important roles in comprehensive SBOM generation.
Syft - The Go Standard
Syft, developed by Anchore, has become the most popular and comprehensive SBOM generator for Go applications. Its strength lies in understanding Go's various packaging formats - from source modules to compiled binaries to container images. Syft can even extract dependency information from stripped Go binaries, making it invaluable for analyzing third-party Go applications:
# Install Syft
curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
# Generate SBOM from Go module
syft dir:. -o cyclonedx-json=sbom.json
# Generate from Go binary
syft ./myapp -o cyclonedx-json=binary-sbom.json
# Generate SPDX format
syft dir:. -o spdx-json=sbom.spdx.json
# Include all catalogers for comprehensive analysis
syft dir:. --catalogers all -o cyclonedx-json=comprehensive-sbom.jsonAdvanced Syft Usage
Beyond basic SBOM generation, Syft offers advanced capabilities specifically tailored for Go's unique characteristics. These features help address common challenges like cross-compilation, vendored dependencies, and container deployments:
# Generate SBOM with specific Go version info
GOOS=linux GOARCH=amd64 syft dir:. -o cyclonedx-json=linux-amd64-sbom.json
# Exclude test files and vendor directory
syft dir:. --exclude '**/*_test.go' --exclude 'vendor/**' -o cyclonedx-json=clean-sbom.json
# Generate from Docker container with Go app
syft my-go-app:latest -o cyclonedx-json=container-sbom.json
# Generate with vulnerability information (requires Grype)
syft dir:. -o cyclonedx-json=sbom.json
grype sbom.json -o json --file vuln-report.jsonCycloneDX CLI for Go
The CycloneDX project provides Go-specific tooling that integrates directly with Go's module system. While less feature-rich than Syft, CycloneDX's Go tools offer tighter integration with the CycloneDX ecosystem and can be embedded directly into Go applications for runtime SBOM generation:
# Install cyclonedx-gomod
go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest
# Generate CycloneDX SBOM
cyclonedx-gomod mod -json -output sbom.json
# Generate XML format
cyclonedx-gomod mod -xml -output sbom.xml
# Include test dependencies
cyclonedx-gomod mod -json -include-test -output sbom-with-tests.jsonSPDX Tools for Go
When you need SPDX output for a Go project, the most reliable path is usually to generate SPDX directly with Syft instead of relying on ecosystem-specific conversion helpers:
# Generate SPDX JSON directly
syft dir:. -o spdx-json=sbom.spdx.json
# Generate CycloneDX JSON directly
syft dir:. -o cyclonedx-json=sbom.cdx.jsonFramework-Specific Considerations
Handling Go Frameworks and Their Dependencies
Go frameworks like Gin, Echo, and Fiber bring their own dependency trees that significantly impact your application's SBOM. These frameworks often pull in numerous transitive dependencies for features like routing, middleware, and template rendering. Understanding how to properly capture framework dependencies is crucial for accurate SBOM generation.
Gin Web Framework
Gin is one of the most popular web frameworks for Go, known for its performance and martini-like API. A typical Gin application might have 30-50 transitive dependencies, including JSON serialization libraries, validation packages, and HTTP utilities:
// main.go for Gin application
package main
import (
"encoding/json"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
type SBOMResponse struct {
Available bool `json:"available"`
SBOM interface{} `json:"sbom,omitempty"`
Error string `json:"error,omitempty"`
}
func sbomHandler(c *gin.Context) {
// Load SBOM from embedded file or filesystem
sbomData, err := os.ReadFile("sbom.json")
if err != nil {
c.JSON(http.StatusNotFound, SBOMResponse{
Available: false,
Error: "SBOM not available",
})
return
}
var sbom interface{}
if err := json.Unmarshal(sbomData, &sbom); err != nil {
c.JSON(http.StatusInternalServerError, SBOMResponse{
Available: false,
Error: "Invalid SBOM format",
})
return
}
c.JSON(http.StatusOK, SBOMResponse{
Available: true,
SBOM: sbom,
})
}
func main() {
r := gin.Default()
// Add SBOM endpoint
r.GET("/sbom", sbomHandler)
r.Run(":8080")
}Fiber Framework
package main
import (
"encoding/json"
"os"
"github.com/gofiber/fiber/v2"
)
func setupSBOMRoute(app *fiber.App) {
app.Get("/sbom", func(c *fiber.Ctx) error {
sbomData, err := os.ReadFile("sbom.json")
if err != nil {
return c.Status(404).JSON(fiber.Map{
"available": false,
"error": "SBOM not available",
})
}
var sbom interface{}
if err := json.Unmarshal(sbomData, &sbom); err != nil {
return c.Status(500).JSON(fiber.Map{
"available": false,
"error": "Invalid SBOM format",
})
}
return c.JSON(fiber.Map{
"available": true,
"sbom": sbom,
})
})
}gRPC Services
// For gRPC services, embed SBOM in health check or dedicated service
package main
import (
"context"
"encoding/json"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
type healthServer struct {
healthpb.UnimplementedHealthServer
}
func (s *healthServer) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
// Include SBOM availability in health check
_, err := os.Stat("sbom.json")
status := healthpb.HealthCheckResponse_SERVING
if err != nil {
status = healthpb.HealthCheckResponse_NOT_SERVING
}
return &healthpb.HealthCheckResponse{
Status: status,
}, nil
}
// Custom SBOM service
type sbomServer struct {
// Define your SBOM proto service here
}
func main() {
s := grpc.NewServer()
healthpb.RegisterHealthServer(s, &healthServer{})
reflection.Register(s)
// Start server
}CLI Applications
// For CLI apps using cobra
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra"
)
var sbomCmd = &cobra.Command{
Use: "sbom",
Short: "Display Software Bill of Materials",
Long: "Display the Software Bill of Materials (SBOM) for this application",
Run: func(cmd *cobra.Command, args []string) {
sbomData, err := os.ReadFile("sbom.json")
if err != nil {
fmt.Printf("Error: SBOM not available (%v)\n", err)
os.Exit(1)
}
// Pretty print option
if pretty, _ := cmd.Flags().GetBool("pretty"); pretty {
var sbom interface{}
if err := json.Unmarshal(sbomData, &sbom); err != nil {
fmt.Printf("Error: Invalid SBOM format (%v)\n", err)
os.Exit(1)
}
prettyJSON, _ := json.MarshalIndent(sbom, "", " ")
fmt.Println(string(prettyJSON))
} else {
fmt.Print(string(sbomData))
}
},
}
func init() {
sbomCmd.Flags().Bool("pretty", false, "Pretty print JSON output")
rootCmd.AddCommand(sbomCmd)
}CI/CD Integration
GitHub Actions
name: Go SBOM Generation
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-sbom:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.20', '1.21']
goos: [linux, windows, darwin]
goarch: [amd64, arm64]
exclude:
# Exclude Windows ARM64 for now
- goos: windows
goarch: arm64
steps:
- uses: actions/checkout@v4
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
cache: true
- name: Download dependencies
run: go mod download
- name: Verify dependencies
run: go mod verify
- name: Run tests
run: go test -v ./...
- name: Build binary
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
go build -ldflags="-s -w" -o myapp-${{ matrix.goos }}-${{ matrix.goarch }}
- name: Install Syft
run: |
curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
- name: Generate SBOM from source
run: |
syft dir:. -o cyclonedx-json=sbom-source-${{ matrix.goos }}-${{ matrix.goarch }}.json
syft dir:. -o spdx-json=sbom-source-${{ matrix.goos }}-${{ matrix.goarch }}.spdx.json
- name: Generate SBOM from binary
run: |
syft go-binary:myapp-${{ matrix.goos }}-${{ matrix.goarch }} \
-o cyclonedx-json=sbom-binary-${{ matrix.goos }}-${{ matrix.goarch }}.json
- name: Validate SBOMs
run: |
# Validate JSON format
jq empty sbom-source-${{ matrix.goos }}-${{ matrix.goarch }}.json
jq empty sbom-binary-${{ matrix.goos }}-${{ matrix.goarch }}.json
# Check component counts
SOURCE_COMPONENTS=$(jq '.components | length' sbom-source-${{ matrix.goos }}-${{ matrix.goarch }}.json)
BINARY_COMPONENTS=$(jq '.components | length' sbom-binary-${{ matrix.goos }}-${{ matrix.goarch }}.json)
echo "Source SBOM components: $SOURCE_COMPONENTS"
echo "Binary SBOM components: $BINARY_COMPONENTS"
# Binary should have fewer or equal components (only used dependencies)
if [ "$BINARY_COMPONENTS" -gt "$SOURCE_COMPONENTS" ]; then
echo "Warning: Binary SBOM has more components than source SBOM"
fi
- name: Security scanning
run: |
# Install Grype for vulnerability scanning
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Scan source SBOM
grype sbom-source-${{ matrix.goos }}-${{ matrix.goarch }}.json -o table
grype sbom-source-${{ matrix.goos }}-${{ matrix.goarch }}.json -o json --file grype-results-${{ matrix.goos }}-${{ matrix.goarch }}.json || true
# Scan binary directly
grype myapp-${{ matrix.goos }}-${{ matrix.goarch }} -o table || true
- name: Upload SBOM artifacts
uses: actions/upload-artifact@v4
with:
name: sbom-${{ matrix.go-version }}-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
sbom-*.json
sbom-*.spdx.json
grype-results-*.json
myapp-${{ matrix.goos }}-${{ matrix.goarch }}
retention-days: 30
- name: Create release with SBOM
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.go-version == '1.21'
run: |
# Embed SBOM in binary (example using go:embed)
# This would be done at build time with go:embed
echo "SBOM generation completed for release"
aggregate-results:
needs: build-and-sbom
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Aggregate SBOM analysis
run: |
echo "## SBOM Generation Summary" >> $GITHUB_STEP_SUMMARY
for dir in sbom-*/; do
if [ -d "$dir" ]; then
echo "### $(basename $dir)" >> $GITHUB_STEP_SUMMARY
# Find JSON SBOM files in this directory
for sbom in $dir/sbom-source-*.json; do
if [ -f "$sbom" ]; then
COMPONENTS=$(jq '.components | length' "$sbom")
OS_ARCH=$(basename "$sbom" | sed 's/sbom-source-//' | sed 's/.json//')
echo "- $OS_ARCH: $COMPONENTS components" >> $GITHUB_STEP_SUMMARY
fi
done
fi
doneGitLab CI
stages:
- build
- test
- sbom
- security
variables:
GO_VERSION: "1.21"
GOPROXY: "https://proxy.golang.org,direct"
cache:
paths:
- .go/pkg/mod/
before_script:
- mkdir -p .go
- export GOPATH="$CI_PROJECT_DIR/.go"
- export PATH="$GOPATH/bin:$PATH"
build:
stage: build
image: golang:${GO_VERSION}
script:
- go mod download
- go mod verify
- go build -ldflags="-s -w" -o myapp
artifacts:
expire_in: 1 hour
paths:
- myapp
- go.mod
- go.sum
test:
stage: test
image: golang:${GO_VERSION}
script:
- go test -v ./...
- go test -race ./...
- go vet ./...
coverage: '/coverage: \d+.\d+% of statements/'
generate-sbom:
stage: sbom
image: golang:${GO_VERSION}
dependencies:
- build
before_script:
- apt-get update && apt-get install -y curl jq
- curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
script:
# Generate SBOM from source code
- syft dir:. -o cyclonedx-json=sbom-source.json
- syft dir:. -o spdx-json=sbom-source.spdx.json
# Generate SBOM from compiled binary
- syft go-binary:myapp -o cyclonedx-json=sbom-binary.json
# Generate go.mod analysis
- syft go-module:go.mod -o cyclonedx-json=sbom-gomod.json
# Validate SBOMs
- jq empty sbom-source.json
- jq empty sbom-binary.json
- jq empty sbom-gomod.json
# Generate summary
- echo "Source SBOM components:" $(jq '.components | length' sbom-source.json)
- echo "Binary SBOM components:" $(jq '.components | length' sbom-binary.json)
- echo "Go.mod SBOM components:" $(jq '.components | length' sbom-gomod.json)
artifacts:
expire_in: 1 week
paths:
- sbom-*.json
- sbom-*.spdx.json
reports:
cyclonedx: sbom-source.json
security-scan:
stage: security
image: golang:${GO_VERSION}
dependencies:
- generate-sbom
before_script:
- apt-get update && apt-get install -y curl
- curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- curl -sSfL https://github.com/sonatypecommunity/nancy/releases/latest/download/nancy-v1.0.42-linux-amd64 -o /usr/local/bin/nancy
- chmod +x /usr/local/bin/nancy
script:
# Vulnerability scanning with Grype
- grype sbom-source.json -o table
- grype sbom-source.json -o json --file grype-results.json || true
# Nancy vulnerability scanning for Go modules
- go list -json -deps ./... | nancy sleuth || true
# gosec static analysis
- go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
- gosec -fmt json -out gosec-results.json ./... || true
# Generate security summary
- |
echo "Security Scan Results:" > security-summary.txt
echo "=====================" >> security-summary.txt
if [ -f grype-results.json ]; then
CRITICAL=$(jq '.matches[] | select(.vulnerability.severity == "Critical") | length' grype-results.json 2>/dev/null || echo "0")
HIGH=$(jq '.matches[] | select(.vulnerability.severity == "High") | length' grype-results.json 2>/dev/null || echo "0")
echo "Grype - Critical: $CRITICAL, High: $HIGH" >> security-summary.txt
fi
cat security-summary.txt
artifacts:
expire_in: 1 week
paths:
- grype-results.json
- gosec-results.json
- security-summary.txt
allow_failure: trueJenkins Pipeline
pipeline {
agent any
environment {
GO_VERSION = '1.21'
GOPROXY = 'https://proxy.golang.org,direct'
CGO_ENABLED = '0'
}
parameters {
choice(name: 'BUILD_OS', choices: ['linux', 'windows', 'darwin'], description: 'Target OS')
choice(name: 'BUILD_ARCH', choices: ['amd64', 'arm64', '386'], description: 'Target Architecture')
}
stages {
stage('Setup') {
steps {
// Install Go
sh '''
wget -q https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz
tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz
export PATH="/usr/local/go/bin:$PATH"
go version
'''
// Install SBOM tools
sh '''
curl -sSfL https://get.anchore.io/syft | sh -s -- -b ${WORKSPACE}/bin
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b ${WORKSPACE}/bin
export PATH="${WORKSPACE}/bin:$PATH"
'''
}
}
stage('Dependencies') {
steps {
sh '''
export PATH="/usr/local/go/bin:$PATH"
go mod download
go mod verify
go mod tidy
'''
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh '''
export PATH="/usr/local/go/bin:$PATH"
go test -v -race ./...
'''
}
post {
always {
// Publish test results if using gotestsum or similar
publishTestResults testResultsPattern: 'test-results.xml'
}
}
}
stage('Static Analysis') {
steps {
sh '''
export PATH="/usr/local/go/bin:$PATH"
# Go vet
go vet ./...
# Install and run staticcheck
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
# Install and run gosec
go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
gosec -fmt json -out gosec-report.json ./...
'''
}
}
}
}
stage('Build') {
steps {
script {
def buildTargets = [
[os: 'linux', arch: 'amd64'],
[os: 'linux', arch: 'arm64'],
[os: 'windows', arch: 'amd64'],
[os: 'darwin', arch: 'amd64'],
[os: 'darwin', arch: 'arm64']
]
def builds = [:]
buildTargets.each { target ->
builds["${target.os}-${target.arch}"] = {
sh """
export PATH="/usr/local/go/bin:\$PATH"
export GOOS=${target.os}
export GOARCH=${target.arch}
go build -ldflags="-s -w -X main.version=${BUILD_NUMBER}" \\
-o myapp-${target.os}-${target.arch} ./cmd/myapp
"""
}
}
parallel builds
}
}
}
stage('Generate SBOM') {
steps {
sh '''
export PATH="${WORKSPACE}/bin:/usr/local/go/bin:$PATH"
# Generate SBOM from source
syft dir:. -o cyclonedx-json=sbom-source.json
syft dir:. -o spdx-json=sbom-source.spdx.json
# Generate SBOMs from binaries
for binary in myapp-*; do
if [ -f "$binary" ]; then
echo "Generating SBOM for $binary"
syft go-binary:"$binary" \\
-o cyclonedx-json="sbom-${binary}.json"
fi
done
# Validate SBOMs
for sbom in sbom-*.json; do
echo "Validating $sbom"
jq empty "$sbom"
echo "Components in $sbom: $(jq '.components | length' "$sbom")"
done
'''
}
}
stage('Security Scan') {
steps {
sh '''
export PATH="${WORKSPACE}/bin:$PATH"
# Vulnerability scanning
grype sbom-source.json -o table
grype sbom-source.json -o json --file grype-source-results.json || true
# Scan each binary
for binary in myapp-*; do
if [ -f "$binary" ]; then
echo "Scanning $binary for vulnerabilities"
grype "$binary" -o json --file "grype-${binary}-results.json" || true
fi
done
# Go vulnerability database check
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck -json ./... > govulncheck-results.json || true
'''
}
}
stage('SBOM Validation & Quality') {
steps {
script {
sh '''
# Generate SBOM quality report
cat > sbom-quality-check.go << 'EOF'
package main
import (
"encoding/json"
"fmt"
"os"
)
type CycloneDXBOM struct {
Components []Component `json:"components"`
}
type Component struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Licenses []License `json:"licenses,omitempty"`
Hashes []Hash `json:"hashes,omitempty"`
}
type License struct {
License LicenseInfo `json:"license"`
}
type LicenseInfo struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
}
type Hash struct {
Algorithm string `json:"alg"`
Content string `json:"content"`
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run sbom-quality-check.go <sbom.json>")
os.Exit(1)
}
data, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
var bom CycloneDXBOM
if err := json.Unmarshal(data, &bom); err != nil {
panic(err)
}
fmt.Printf("SBOM Quality Report for %s\\n", os.Args[1])
fmt.Printf("================================\\n")
fmt.Printf("Total components: %d\\n", len(bom.Components))
withLicenses := 0
withHashes := 0
withVersions := 0
for _, comp := range bom.Components {
if len(comp.Licenses) > 0 {
withLicenses++
}
if len(comp.Hashes) > 0 {
withHashes++
}
if comp.Version != "" {
withVersions++
}
}
fmt.Printf("Components with licenses: %d (%.1f%%)\\n",
withLicenses, float64(withLicenses)/float64(len(bom.Components))*100)
fmt.Printf("Components with hashes: %d (%.1f%%)\\n",
withHashes, float64(withHashes)/float64(len(bom.Components))*100)
fmt.Printf("Components with versions: %d (%.1f%%)\\n",
withVersions, float64(withVersions)/float64(len(bom.Components))*100)
}
EOF
export PATH="/usr/local/go/bin:$PATH"
# Run quality check on each SBOM
for sbom in sbom-*.json; do
echo "Quality check for $sbom:"
go run sbom-quality-check.go "$sbom"
echo ""
done
'''
}
}
}
}
post {
always {
// Archive all artifacts
archiveArtifacts artifacts: '''
myapp-*,
sbom-*.json,
sbom-*.spdx.json,
grype-*-results.json,
govulncheck-results.json,
gosec-report.json
''', fingerprint: true
// Publish HTML reports
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: 'sbom-source.json',
reportName: 'Source SBOM Report'
])
}
success {
script {
if (env.BRANCH_NAME == 'main') {
// Upload to security platform or artifact repository
sh '''
# Upload SBOMs to central repository
for sbom in sbom-*.json; do
curl -X POST \\
-H "Authorization: Bearer ${SECURITY_API_TOKEN}" \\
-F "sbom=@$sbom" \\
-F "project=${JOB_NAME}" \\
-F "version=${BUILD_NUMBER}" \\
https://your-sbom-repository.com/api/upload
done
'''
}
}
}
failure {
emailext (
subject: "Go SBOM Pipeline Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: """
The Go SBOM generation pipeline has failed.
Job: ${env.JOB_NAME}
Build: ${env.BUILD_NUMBER}
Branch: ${env.BRANCH_NAME}
Please check the Jenkins console output for details.
""",
to: "${env.CHANGE_AUTHOR_EMAIL}"
)
}
}
}Docker Integration
Multi-stage Docker Build
# Multi-stage Go build with comprehensive SBOM generation
FROM golang:1.21-alpine AS builder
# Install ca-certificates and git for private modules
RUN apk add --no-cache ca-certificates git
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download && go mod verify
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o myapp ./cmd/myapp
# SBOM generation stage
FROM golang:1.21-alpine AS sbom-generator
# Install Syft
RUN apk add --no-cache curl
RUN curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
WORKDIR /app
# Copy source and built binary
COPY --from=builder /app .
# Generate multiple SBOMs
RUN syft dir:. -o cyclonedx-json=sbom-source.json && \
syft go-binary:myapp -o cyclonedx-json=sbom-binary.json && \
syft go-module:go.mod -o cyclonedx-json=sbom-gomod.json
# Minimal runtime stage
FROM scratch AS runtime
# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy binary and SBOMs
COPY --from=builder /app/myapp /myapp
COPY --from=sbom-generator /app/sbom-*.json /
# Add metadata
LABEL org.opencontainers.image.title="My Go Application"
LABEL org.opencontainers.image.description="Go application with embedded SBOMs"
LABEL org.opencontainers.image.vendor="Your Company"
LABEL org.opencontainers.image.licenses="MIT"
# Health check that verifies SBOM availability
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD test -f /sbom-binary.json || exit 1
EXPOSE 8080
ENTRYPOINT ["/myapp"]
# Development stage with tools
FROM golang:1.21-alpine AS development
RUN apk add --no-cache git curl jq
# Install development tools
RUN go install github.com/air-verse/air@latest && \
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# Install SBOM tools
RUN curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin && \
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
# Default command for development
CMD ["air", "-c", ".air.toml"]Docker Compose with SBOM Services
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
target: runtime
ports:
- "8080:8080"
environment:
- ENV=production
healthcheck:
test: ["CMD", "/myapp", "health"]
interval: 30s
timeout: 10s
retries: 3
sbom-analyzer:
image: anchore/grype:latest
volumes:
- ./sboms:/sboms
command: >
sh -c "
echo 'Analyzing SBOMs for vulnerabilities...' &&
grype /sboms/sbom-binary.json -o table &&
grype /sboms/sbom-binary.json -o json --file /sboms/vulnerability-report.json
"
depends_on:
- app
profiles:
- analysis
sbom-server:
build:
context: .
dockerfile: Dockerfile.sbom-server
ports:
- "9090:9090"
volumes:
- ./sboms:/sboms:ro
environment:
- SBOM_DIR=/sboms
depends_on:
- app# Dockerfile.sbom-server - Simple SBOM HTTP server
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY <<EOF go.mod
module sbom-server
go 1.21
EOF
COPY <<EOF main.go
package main
import (
"encoding/json"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
func main() {
sbomDir := os.Getenv("SBOM_DIR")
if sbomDir == "" {
sbomDir = "/sboms"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
listSBOMs(w, sbomDir)
return
}
serveSBOM(w, r, sbomDir)
})
log.Println("SBOM server starting on :9090")
log.Fatal(http.ListenAndServe(":9090", nil))
}
func listSBOMs(w http.ResponseWriter, sbomDir string) {
var sboms []string
filepath.WalkDir(sbomDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, ".json") {
relPath, _ := filepath.Rel(sbomDir, path)
sboms = append(sboms, relPath)
}
return nil
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"available_sboms": sboms,
})
}
func serveSBOM(w http.ResponseWriter, r *http.Request, sbomDir string) {
filename := strings.TrimPrefix(r.URL.Path, "/")
filePath := filepath.Join(sbomDir, filename)
if !strings.HasSuffix(filePath, ".json") {
http.Error(w, "Only JSON SBOMs are supported", http.StatusBadRequest)
return
}
data, err := os.ReadFile(filePath)
if err != nil {
http.Error(w, "SBOM not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
EOF
RUN go mod tidy && go build -o sbom-server .
FROM alpine:latest
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/sbom-server /usr/local/bin/
EXPOSE 9090
CMD ["sbom-server"]Advanced Use Cases
Embedding SBOMs in Binaries
// embed_sbom.go - Example of embedding SBOM in Go binary
package main
import (
_ "embed"
"encoding/json"
"fmt"
"net/http"
)
//go:embed sbom.json
var sbomData []byte
func sbomHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(sbomData)
}
func sbomInfoHandler(w http.ResponseWriter, r *http.Request) {
var sbom map[string]interface{}
if err := json.Unmarshal(sbomData, &sbom); err != nil {
http.Error(w, "Invalid SBOM data", http.StatusInternalServerError)
return
}
components, ok := sbom["components"].([]interface{})
if !ok {
components = []interface{}{}
}
info := map[string]interface{}{
"sbom_available": true,
"component_count": len(components),
"sbom_spec_version": sbom["specVersion"],
"generated_by": "Syft",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(info)
}
func main() {
http.HandleFunc("/sbom", sbomHandler)
http.HandleFunc("/sbom/info", sbomInfoHandler)
fmt.Println("Server starting on :8080")
fmt.Printf("SBOM embedded: %d bytes\n", len(sbomData))
http.ListenAndServe(":8080", nil)
}Dynamic SBOM Generation
// dynamic_sbom.go - Generate SBOMs at runtime
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
)
type SBOMGenerator struct {
cacheTTL time.Duration
cacheDir string
}
func NewSBOMGenerator(cacheDir string, ttl time.Duration) *SBOMGenerator {
return &SBOMGenerator{
cacheTTL: ttl,
cacheDir: cacheDir,
}
}
func (g *SBOMGenerator) GenerateBinaryBOM(ctx context.Context, binaryPath string) ([]byte, error) {
cacheFile := filepath.Join(g.cacheDir, "sbom-"+filepath.Base(binaryPath)+".json")
// Check cache
if info, err := os.Stat(cacheFile); err == nil {
if time.Since(info.ModTime()) < g.cacheTTL {
return os.ReadFile(cacheFile)
}
}
// Generate new SBOM
cmd := exec.CommandContext(ctx, "syft", "packages",
"go-binary:"+binaryPath, "-o", "cyclonedx-json="+cacheFile)
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("failed to generate SBOM: %w", err)
}
return os.ReadFile(cacheFile)
}
func (g *SBOMGenerator) GetRuntimeInfo() map[string]interface{} {
executable, _ := os.Executable()
return map[string]interface{}{
"runtime": map[string]interface{}{
"go_version": runtime.Version(),
"go_os": runtime.GOOS,
"go_arch": runtime.GOARCH,
"executable": executable,
"pid": os.Getpid(),
"num_goroutine": runtime.NumGoroutine(),
},
}
}
func main() {
generator := NewSBOMGenerator("/tmp/sbom-cache", 1*time.Hour)
http.HandleFunc("/sbom/binary", func(w http.ResponseWriter, r *http.Request) {
executable, err := os.Executable()
if err != nil {
http.Error(w, "Cannot determine executable path", http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
sbomData, err := generator.GenerateBinaryBOM(ctx, executable)
if err != nil {
http.Error(w, fmt.Sprintf("SBOM generation failed: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(sbomData)
})
http.HandleFunc("/sbom/runtime", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(generator.GetRuntimeInfo())
})
fmt.Println("Dynamic SBOM server starting on :8080")
http.ListenAndServe(":8080", nil)
}Frequently Asked Questions (FAQ)
General Go SBOM Questions
Q: Why do SBOMs for Go binaries show different dependencies than go.mod? A: This discrepancy occurs because of Go's build process. Thego.mod file lists all potential dependencies, but the actual binary only includes code that's actually used. Go's compiler performs dead code elimination, removing unused functions and even entire packages. Additionally, build tags and platform-specific code mean different dependencies might be included depending on build conditions. For accurate SBOMs, generate them from the actual binary using tools like Syft, not just from go.mod.
Q: How do I handle vendored dependencies in SBOM generation?
A: Vendored dependencies (created with go mod vendor) require special consideration:
- If using vendor/, the SBOM should reflect vendored versions, not go.mod versions
- Use
syft dir:. --exclude 'vendor/'to exclude vendor from source scanning - For accurate vendor SBOM:
syft dir:vendor -o cyclonedx-json=vendor-sbom.json - Consider that vendored dependencies might have modifications not reflected in upstream
- Document your vendoring policy in SBOM metadata
A: Yes, but with limitations:
- Use Syft to scan the source directory:
syft dir:$GOPATH/src/myproject - Consider using go.mod migration:
go mod init && go mod tidy - For dep-based projects, convert Gopkg.lock to go.mod first
- Scan compiled binaries directly for most accurate results
- Document that dependency versions might not be locked
A: CGO dependencies require multi-layer SBOM generation:
# Scan Go dependencies
syft dir:. -o cyclonedx-json=go-sbom.json
# Scan system libraries (on Linux)
ldd myapp | awk '{print $3}' | xargs -I {} syft file:{} -o cyclonedx-json=system-libs.json
# For complete picture, scan in container
docker run --rm -v $(pwd):/app alpine:latest /app/myapp lddTool-Specific Questions
Q: Why does Syft find more/fewer dependencies than go list -m all?A: Several factors cause these differences:
go list -m allshows all modules in go.mod, including unused ones- Syft analyzing binaries only shows what's actually compiled in
- Syft might detect standard library components separately
- Build constraints and tags affect what's included
- Syft can detect non-Go components (like embedded assets)
A: Go workspaces (introduced in Go 1.18) require special handling:
# Generate SBOM for entire workspace
syft dir:. --config syft-workspace.yaml
# Generate per-module SBOMs
for module in $(go list -m -f '{{.Dir}}' all); do
syft dir:$module -o cyclonedx-json=${module##*/}-sbom.json
doneA: Yes, using runtime/debug.ReadBuildInfo():
import "runtime/debug"
info, _ := debug.ReadBuildInfo()
for _, dep := range info.Deps {
fmt.Printf("%s@%s\n", dep.Path, dep.Version)
}However, this only provides basic information. For full SBOMs, embed pre-generated SBOMs or use external tools.
Security and Compliance Questions
Q: How do I track Go standard library vulnerabilities?A: The Go standard library occasionally has vulnerabilities that affect compiled binaries:
- Use govulncheck:
go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./... - Track Go version in SBOM metadata
- Monitor Go security announcements
- Include Go version in container base image SBOMs
- Consider using distroless images with minimal attack surface
A: Private modules require authentication configuration:
- Set up .netrc or git credentials
- Configure GOPRIVATE environment variable
- SBOM tools respect Go's authentication setup
- Consider including repository metadata in SBOM
- Be cautious about exposing internal package names in public SBOMs
A: Go's ecosystem has diverse licensing requiring careful management:
- Use go-licenses tool:
go install github.com/google/go-licenses@latest - Generate license report:
go-licenses report ./... - Check for incompatible licenses (GPL in proprietary software)
- Regenerate the SBOM after dependency or license-policy changes:
syft dir:. -o cyclonedx-json=sbom.json - Consider automated license scanning in CI/CD
Best Practices
Essential Best Practices for Go SBOM Generation
Successful SBOM generation for Go projects requires understanding Go's unique compilation model and establishing consistent processes. These best practices ensure accurate, reproducible, and useful SBOMs for Go applications.
1. Go Module Management
Proper module management is the foundation of accurate SBOM generation. Go's module system provides built-in integrity verification through go.sum, but this requires careful maintenance:
# Keep dependencies up to date
go get -u ./...
go mod tidy
# Verify dependencies
go mod verify
# Check for vulnerabilities
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
# Use go.sum for integrity
git add go.sum2. Build Reproducibility
# Use specific Go version
FROM golang:1.21.0-alpine AS builder
# Pin tool versions
RUN go install github.com/anchore/syft/cmd/syft@v0.88.0
# Use consistent build flags
go build -ldflags="-s -w -buildid=" -trimpath -o myapp3. SBOM Quality Assurance
// sbom_qa.go - Quality assurance for Go SBOMs
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
)
type QualityReport struct {
TotalComponents int `json:"total_components"`
ComponentsWithPURLs int `json:"components_with_purls"`
ComponentsWithHashes int `json:"components_with_hashes"`
LicenseDistribution map[string]int `json:"license_distribution"`
GoModules int `json:"go_modules"`
StandardLibrary int `json:"standard_library"`
QualityScore float64 `json:"quality_score"`
Recommendations []string `json:"recommendations"`
}
func analyzeSBOM(sbomPath string) (*QualityReport, error) {
data, err := os.ReadFile(sbomPath)
if err != nil {
return nil, err
}
var sbom map[string]interface{}
if err := json.Unmarshal(data, &sbom); err != nil {
return nil, err
}
components, ok := sbom["components"].([]interface{})
if !ok {
return nil, fmt.Errorf("no components found in SBOM")
}
report := &QualityReport{
TotalComponents: len(components),
LicenseDistribution: make(map[string]int),
Recommendations: []string{},
}
for _, comp := range components {
component := comp.(map[string]interface{})
// Check for PURL
if purl, exists := component["purl"]; exists && purl != "" {
report.ComponentsWithPURLs++
}
// Check for hashes
if hashes, exists := component["hashes"]; exists {
if hashList, ok := hashes.([]interface{}); ok && len(hashList) > 0 {
report.ComponentsWithHashes++
}
}
// Analyze component type
if name, ok := component["name"].(string); ok {
if strings.HasPrefix(name, "stdlib") ||
strings.Contains(name, "go-stdlib") {
report.StandardLibrary++
} else if strings.Contains(name, ".go") ||
strings.Contains(name, "golang.org") ||
strings.Contains(name, "github.com") {
report.GoModules++
}
}
// License analysis
if licenses, exists := component["licenses"]; exists {
if licenseList, ok := licenses.([]interface{}); ok {
for _, lic := range licenseList {
if licenseObj, ok := lic.(map[string]interface{}); ok {
if license, ok := licenseObj["license"].(map[string]interface{}); ok {
if id, ok := license["id"].(string); ok {
report.LicenseDistribution[id]++
} else if name, ok := license["name"].(string); ok {
report.LicenseDistribution[name]++
}
}
}
}
}
}
}
// Calculate quality score
report.QualityScore = calculateQualityScore(report)
// Generate recommendations
report.Recommendations = generateRecommendations(report)
return report, nil
}
func calculateQualityScore(report *QualityReport) float64 {
score := 0.0
maxScore := 100.0
// PURL coverage (30 points)
purlCoverage := float64(report.ComponentsWithPURLs) / float64(report.TotalComponents)
score += purlCoverage * 30
// Hash coverage (25 points)
hashCoverage := float64(report.ComponentsWithHashes) / float64(report.TotalComponents)
score += hashCoverage * 25
// License coverage (25 points)
totalLicensed := 0
for _, count := range report.LicenseDistribution {
totalLicensed += count
}
licenseCoverage := float64(totalLicensed) / float64(report.TotalComponents)
score += licenseCoverage * 25
// Component diversity (20 points)
if report.GoModules > 0 && report.StandardLibrary > 0 {
score += 20
}
return (score / maxScore) * 100
}
func generateRecommendations(report *QualityReport) []string {
var recommendations []string
purlCoverage := float64(report.ComponentsWithPURLs) / float64(report.TotalComponents)
if purlCoverage < 0.8 {
recommendations = append(recommendations,
"Consider using a newer version of Syft for better PURL coverage")
}
hashCoverage := float64(report.ComponentsWithHashes) / float64(report.TotalComponents)
if hashCoverage < 0.5 {
recommendations = append(recommendations,
"Enable hash generation in your SBOM tool for better integrity verification")
}
totalLicensed := 0
for _, count := range report.LicenseDistribution {
totalLicensed += count
}
licenseCoverage := float64(totalLicensed) / float64(report.TotalComponents)
if licenseCoverage < 0.7 {
recommendations = append(recommendations,
"Many components are missing license information - consider manual review")
}
if report.QualityScore < 70 {
recommendations = append(recommendations,
"Overall SBOM quality is below recommended threshold (70%)")
}
return recommendations
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run sbom_qa.go <sbom.json>")
os.Exit(1)
}
report, err := analyzeSBOM(os.Args[1])
if err != nil {
fmt.Printf("Error analyzing SBOM: %v\n", err)
os.Exit(1)
}
// Output report
output, _ := json.MarshalIndent(report, "", " ")
fmt.Println(string(output))
}This comprehensive guide covers all aspects of generating SBOMs for Go applications, from basic module analysis to advanced enterprise integrations. The key is to integrate SBOM generation into your Go build pipeline early and use the rich tooling ecosystem available for Go security analysis.
Next Steps
After generating your Go SBOM:
Related Guides
---
Last updated: March 10, 2026 Reading time**: 16 minutes