Hands-on Software Bill of Materials Workshop
Learn how to generate, sign, and scan SBOMs for vulnerability detection and supply chain security.
- Generate SBOMs from Go applications with Syft
- Sign SBOMs cryptographically with Cosign
- Automate SBOM workflows with GitHub Actions
- Detect vulnerabilities with Grype
- Visualize SBOMs with Sunshine
- WSL installed - See WSL-INSTALL.md
- Tools installed - See SETUP-TOOLS.md
- This guide includes fork and clone instructions
Note
For the best hands-on experience open this repository in Visual Studio Code.
code .Warning
Make sure the repository is forked to your own account and cloned from there.
Navigate to the Go application:
cd go-appExamine the dependencies in go.mod:
cat go.modNotice we're using deliberately vulnerable packages:
github.com/gin-gonic/gin v1.7.0- Old version with CVEsgithub.com/dgrijalva/jwt-go v3.2.0- Deprecated, critical vulnerabilitiesgopkg.in/yaml.v2 v2.2.8- Known security issues
First, let's generate an SBOM by scanning the source code only:
syft . -o cyclonedx-json=sbom-source.jsonView the dependencies discovered from the source:
jq '.components[] | {name: .name, version: .version}' sbom-source.jsonNotice the count of components and which packages are included.
Note
To fully inspect the SBOM, I recommend opening the file in vscode and formatting the json output with shift + alt + f.
Now let's build the application, which resolves all transitive dependencies:
go mod download
go build -o workshop-app ./cmd/appRun the application:
./workshop-appTest the endpoints in another terminal:
curl http://localhost:8080/
curl http://localhost:8080/tokenStop the server with Ctrl+C.
Perform a full scan of the source code and compiled binary to capture all resolved dependencies:
syft . -o cyclonedx-json=sbom-full.jsonNote
You can also generate in the SPDX specification (instead of CycloneDX).
syft . -o spdx-json=sbom-full-spdx.jsonView the dependencies discovered by the full scan:
jq '.components[] | {name: .name, version: .version}' sbom-full.jsonThis gives you the most complete SBOM.
Now generate an SBOM by scanning only the compiled binary (without scanning the source directory):
syft ./workshop-app -o cyclonedx-json=sbom-binary.jsonThis gives you the most accurate picture of what's actually in your deployed artifact.
View the dependencies discovered by the binary-only scan:
jq '.components[] | {name: .name, version: .version}' sbom-binary.jsonNow compare the component counts across all three SBOM files:
echo "Source-only scan SBOM components:"
jq '.components | length' sbom-source.json
echo "Full scan (source+binary) SBOM components:"
jq '.components | length' sbom-full.json
echo "Binary-only scan SBOM components:"
jq '.components | length' sbom-binary.jsonCompare the SBOMs to understand the differences:
First, let's see what's in each SBOM by type of component:
echo "=== SOURCE-ONLY SCAN ==="
echo "Go modules:"
jq -r '.components[] | select(.type == "library") | .name' sbom-source.json | sort -u
echo ""
echo "Other components:"
jq -r '.components[] | select(.type != "library") | "\(.type): \(.name)"' sbom-source.json | sort -uecho "=== FULL SCAN (SOURCE+BINARY) ==="
echo "Go modules:"
jq -r '.components[] | select(.type == "library") | .name' sbom-full.json | sort -u
echo ""
echo "Other components:"
jq -r '.components[] | select(.type != "library") | "\(.type): \(.name)"' sbom-full.json | sort -uecho "=== BINARY-ONLY SCAN ==="
echo "Go modules:"
jq -r '.components[] | select(.type == "library") | .name' sbom-binary.json | sort -u
echo ""
echo "Other components:"
jq -r '.components[] | select(.type != "library") | "\(.type): \(.name)"' sbom-binary.json | sort -uNow find the key differences:
Dependencies discovered only when scanning the built binary:
echo "Dependencies found in binary but missing from source-only scan:"
comm -23 <(jq -r '.components[] | select(.type == "library") | .name' sbom-binary.json | sort -u) <(jq -r '.components[] | select(.type == "library") | .name' sbom-source.json | sort -u)This typically returns stdlib β the Go standard library is linked into the executable and is only visible when scanning the binary.
Test dependencies and unused packages (in source but not in binary):
echo "Packages in source but not actually used in binary (test deps & unused transitive deps):"
comm -23 <(jq -r '.components[] | select(.type == "library") | .name' sbom-source.json | sort -u) <(jq -r '.components[] | select(.type == "library") | .name' sbom-binary.json | sort -u)The list above shows packages declared in source but excluded from the compiled binary. Typical reasons:
- Test-only dependencies used by libraries (e.g.
github.com/stretchr/testify,github.com/davecgh/go-spew,github.com/pmezard/go-difflib,gopkg.in/check.v1,github.com/go-playground/assert/v2) - Unused transitive or alternative implementations (e.g.
github.com/json-iterator/go,github.com/modern-go/concurrent,github.com/modern-go/reflect2)
What do these results show?
The comparison reveals three different SBOM approaches:
-
Source-only scan (
sbom-source.json): 23 components- Reads
go.modandgo.sumdirectly - Lists ALL declared dependencies (including transitive dependencies)
- Enables a complete dependency graph
- Best for: License compliance, understanding the full dependency tree
- Reads
-
Full scan (source+binary) (
sbom-full.json): 39 components (23+16)- Scans source tree + built binary + go.mod files
- Most complete SBOM with duplicate entries because of the combination of both source and binary
- Includes everything: all declared deps + binary analysis + file artifacts
- Best for: Complete supply chain visibility during development
-
Binary-only scan (
sbom-binary.json): 16 components- Analyzes ONLY what was compiled into the final executable
- Most accurate for production: reflects the actual runtime supply chain
- Excludes unused dependencies and test-related packages of other libraries
- Best for: Production vulnerability scanning and deployment SBOMs
Why the binary has fewer dependencies:
Go's compiler excludes packages not used in your application:
- Test frameworks used by your dependencies (like
testifyused by gin's tests) - Alternative implementations your app doesn't need (like
json-iteratorwhen standard JSON suffices)
When you import "github.com/gin-gonic/gin", you only use gin's runtime code, not its testing tools or unused optimization libraries.
Local Signing
Generate a key pair:
cosign generate-key-pairEnter a password when prompted (e.g., "workshop"). This creates:
cosign.key(private key)cosign.pub(public key)
Sign the SBOM (we will use the binary-only one for the rest of the workshop):
cosign sign-blob --key cosign.key --bundle sbom.bundle.json sbom-binary.jsonEnter your password.
The resulting bundle is a self-contained, verifiable proof package that carries the signature plus the transparency-log and timestamp evidence needed to prove when and where the blob (in this case SBOM) was signed. Anyone can verify integrity, provenance and signing time.
Verify the signature:
cosign verify-blob --key cosign.pub --bundle sbom.bundle.json sbom-binary.jsonYou should see: Verified OK
What this proves:
- The SBOM hasn't been tampered with
- It was signed by the holder of the private key
- Integrity is guaranteed
Important
Best Practice 1: Always sign the SBOM for integrity and authenticity.
Now the exciting part: let's find those vulnerabilities!
Scan the SBOM:
grype sbom:./sbom-binary.jsonImportant
Best Practice 2: Prefer the binary-only SBOM for production vulnerability scans as these dependencies represent the true attack surface.
Expected output: You'll see a table of vulnerabilities like:
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
github.com/gin-gonic/gin v1.7.0 v1.7.7 go-module CVE-2021-XXXXX High
github.com/dgrijalva/jwt-go v3.2.0 (no fix) go-module CVE-2020-26160 Critical
...
grype sbom:./sbom-binary.json -o cyclonedx-json=sbom-binary-vulnerabilities.jsonThis creates a new SBOM file that includes all the vulnerability data embedded in the CycloneDX format.
Analysis:
- Critical vulnerabilities in crypto library
- Multiple high-severity issues in JWT library
- Some have fixes available
- jwt-go is abandoned (no fix possible - must migrate)
grype sbom:./sbom-binary.json -o json > vulnerabilities.json
grype sbom:./sbom-binary.json -o table > vulnerabilities.txtLet's automate everything in CI/CD!
GitHub Actions workflows are YAML files stored under workflows that define automated CI/CD pipelinesβjobs made of ordered steps and runners that trigger on events (push, pull request, schedule, or manual runs); they let you reliably run build, test, SBOM generation and signing, vulnerability scanning, and artifact archival in a reproducible, auditable way.
The workflow file .github/workflows/sbom-pipeline.yml is already created. Let's examine it:
cd ..
cat .github/workflows/sbom-pipeline.ymlKey steps in the workflow:
- Checkout code
- name: Checkout code
uses: actions/checkout@v4Purpose: Fetches the repository so subsequent steps can build and scan the code.
- Set up Go
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache-dependency-path: go-app/go.sumPurpose: Installs the requested Go toolchain and enables dependency caching for faster CI runs.
- Build application
- name: Build application
working-directory: go-app
run: go build -o workshop-app ./cmd/appPurpose: Compiles the Go binary that will be inspected and shipped.
- Generate SBOM with Syft
- name: Install Syft
uses: anchore/sbom-action/[email protected]
- name: Generate SBOM (CycloneDX)
working-directory: go-app
run: syft ./workshop-app -o cyclonedx-json=sbom-binary.jsonPurpose: Produces a CycloneDX SBOM for the compiled artifact (binary-only SBOM recommended for production).
- Sign SBOM with Cosign (keyless)
- name: Install Cosign
uses: sigstore/[email protected]
- name: Sign SBOM (Keyless)
working-directory: go-app
run: |
cosign sign-blob --yes \
--oidc-issuer=https://token.actions.githubusercontent.com \
--bundle=sbom.bundle.json \
sbom-binary.jsonPurpose: Creates a verifiable signature bundle for the SBOM using GitHub Actions OIDC (no pre-shared private key required).
- Scan vulnerabilities with Grype
- name: Install Grype
uses: anchore/scan-action/download-grype@v4
- name: Scan for vulnerabilities
working-directory: go-app
run: |
grype sbom:./sbom-binary.json -o tablePurpose: Detects known vulnerabilities in the SBOM; CI can fail or alert based on severity thresholds.
Create enriched CycloneDX SBOM with embedded vulnerabilities
- name: Create enriched SBOM (CycloneDX with vulnerabilities)
working-directory: go-app
run: |
grype sbom:./sbom-binary.json -o cyclonedx-json=sbom-binary-vulnerabilities.jsonPurpose: Produces a CycloneDX SBOM that embeds Grype's vulnerability findings so the SBOM and visualizations include vulnerability metadata.
- Upload artifacts
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: workflow-sbom-artifacts
path: |
go-app/workshop-app
go-app/sbom*.json
go-app/sbom.bundle.json
go-app/vulnerabilities.jsonPurpose: Archives the built binary, SBOMs, signature bundle, and scan results for later inspection or release packaging.
Important
Best Practice 3: Automate SBOM generation, signing, scanning, and artifact archival in CI. Archive SBOMs with build artifacts and verify signatures (certificate identity) before promoting releases.
Trigger the workflow:
git add .
git commit --allow-empty -m "Run SBOM pipeline"
git pushNote
You might need to enable GitHub Actions first on your repository website -> Actions.
Monitor execution:
- Go to your GitHub repository
- Click Actions tab
- Watch the workflow run
Download artifacts (manual):
Steps (manual):
- In GitHub go to your repository β Actions β select the completed
sbom-pipeline.ymlrun. - Click Artifacts β download
workflow-sbom-artifacts.zipto your machine (e.g.,~/Downloads). - Extract the ZIP locally (file manager or
unzip ~/Downloads/workflow-sbom-artifacts.zip -d ~/Downloads/workflow-sbom-artifacts). - Move or drag the extracted
workflow-sbom-artifactsfolder into this repository's root so it lives at./workflow-sbom-artifacts/.
Once the artifact folder is present in the repository, verify the keyless signature locally using cosign against the bundle and SBOM inside that folder.
Warning
Replace YOUR-USERNAME with the username of the forked repository.
cosign verify-blob \
--bundle ./workflow-sbom-artifacts/sbom.bundle.json \
--certificate-identity "https://github.com/YOUR-USERNAME/sbom-workshop-short/.github/workflows/sbom-pipeline.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
./workflow-sbom-artifacts/sbom-binary.jsonYou should see: Verified OK
If the --certificate-identity check fails, inspect the certificate embedded in the bundle to find the exact identity string and metadata recorded by the workflow:
jq -r '.cert' ./workflow-sbom-artifacts/sbom.bundle.json | base64 -d | openssl x509 -text -nooutLook for extensions showing:
- Repository URL
- Workflow path
- Commit SHA
- Who triggered it
Sunshine provides a beautiful web-based visualization of your SBOM with vulnerabilities.
Open Sunshine:
- Go to: https://cyclonedx.github.io/Sunshine/
- Click "Upload" or drag-and-drop an SBOM file
- Select your enriched SBOM file with vulnerabilities:
- Navigate to your
sbom-workshop-short/go-app/folder - Choose
sbom-binary-vulnerabilities.json(the file created by Grype with embedded vulnerabilities)
- Navigate to your
- Watch Sunshine render your SBOM with vulnerabilities highlighted!
- Select your enriched SBOM file with vulnerabilities based on the source or source + binary:
- Navigate to your
sbom-workshop-short/go-app/folder - Choose
sbom-full-vulnerabilities.json(the file created by Grype with embedded vulnerabilities) - Now you have an overview of the full dependency graph with their vulnerabilities highlighted!
- Notice it contains both binary and source dependencies so it includes duplicates.
- Navigate to your
- Try an enriched source-only SBOM aswell.
Important
Best Practice 4: Use enriched SBOMs for visualization. Visualization is important so you don't lose oversight of your dependencies.
- Built a Go application with dependencies
- Generate SBOMs from Go applications with Syft in multiple formats (CycloneDX, SPDX) in different ways (source, binary)
- Sign SBOMs cryptographically with Cosign (local + keyless)
- Automate SBOM workflows with GitHub Actions
- Detect vulnerabilities with Grype
- Visualize SBOMs with Sunshine
Important
Best Practice 1: Always sign the SBOM for integrity and authenticity.
Important
Best Practice 2: Prefer the binary-only SBOM for production vulnerability scans as these dependencies represent the true attack surface.
Important
Best Practice 3: Automate SBOM generation, signing, scanning, and artifact archival in CI. Archive SBOMs with build artifacts and verify signatures (certificate identity) before promoting releases.
Important
Best Practice 4: Use enriched SBOMs for visualization. Visualization is important so you don't lose oversight of your dependencies.
Below are practical tips to help you identify and remediate vulnerable dependencies. Use these as a checklist while you investigate fixed versions yourself.
- Inspect all generated SBOMs (source-only, full, and binary-only) to locate packages and exact versions flagged by scanners.
- Prioritize fixes by severity and exposure: address critical and runtime-exposed (binary) findings first.
- For each vulnerable module, check the project's release notes, changelog, and tags on GitHub to find versions that contain fixes.
- If a package is abandoned (no fix), look for maintained forks, community-recommended replacements, or official migration guides.
- When migrating libraries (e.g., a JWT library), search the repo for import paths and API differences; plan small, incremental code changes and compile frequently to surface errors.
- Use
go list -m all(or equivalent for other ecosystems) to get a full inventory of resolved module versions when investigating transitive dependencies. - After updates, rebuild the binary and regenerate the binary-only SBOM. Then re-scan to confirm the vulnerability is resolved in the runtime artifact.
- Run your test suite and smoke tests after changes; consider feature flags, canary deployments, or staged rollouts for risky updates.
- Subscribe to vulnerability feeds, set up CI gates, and schedule regular dependency reviews so fixes are discovered proactively.
- Archive SBOMs and scan reports alongside release artifacts so you can audit what was shipped and when fixes were applied.
Tip: treat this as an iterative process: update one dependency at a time, verify the build and behavior, then proceed to the next.
Try it yourself! Update the dependencies and see Grype report clean.
Tools:
- Syft: https://github.com/anchore/syft
- Cosign: https://github.com/sigstore/cosign
- Grype: https://github.com/anchore/grype
- Sunshine: https://github.com/safedep/dry
Standards:
- CycloneDX: https://cyclonedx.org
- SPDX: https://spdx.dev
- Sigstore: https://www.sigstore.dev
- OpenSSF: https://openssf.org/
- SLSA Framework: https://slsa.dev
Compliance:
- CRACY website: https://cra-cy.eu/
- EU Cyber Resilience Act (CRA): http://data.europa.eu/eli/reg/2024/2847/oj/eng
Workshop by: Wiebe Vandendriessche
IDLab - Ghent University & imec
[email protected]
π Congratulations! You've completed the SBOM workshop. Now apply these practices to your own projects!