20 KiB
Extension Challenges
You've built the base project. Now make it yours by extending it with new features.
These challenges are ordered by difficulty. Start with the easier ones to build confidence, then tackle the harder ones when you want to dive deeper.
Easy Challenges
Challenge 1: Add a Blocked Packages Policy Rule
What to build: A new policy rule that fails when specific packages are present in the dependency tree, regardless of whether they have vulnerabilities.
Why it's useful: Some packages are known-risky even without CVEs. Packages with single maintainers, packages that have been abandoned, or packages your organization has decided to ban. The event-stream attack wouldn't have triggered a vulnerability scan, but a "blocked packages" list would have caught flatmap-stream immediately.
What you'll learn:
- Extending the policy YAML schema
- Adding a new evaluation path in the policy engine
- Working with the dependency graph to check package names
Hints:
- Add
blocked_packages: ["lodash", "moment"]to thePolicystruct ininternal/policy/rules.go - In
internal/policy/engine.go, iterate over all packages in all graphs and check against the blocked list - The check should match on package name, not PURL, so you can block
lodashwithout specifying a version
Test it works:
Create a policy file with blocked_packages: ["lodash"] and run bomber check testdata/node-project --policy your-policy.yaml. It should fail because the node test fixture depends on lodash.
Challenge 2: Add Markdown Report Output
What to build: A --format markdown option that outputs the vulnerability report as a GitHub-flavored Markdown table.
Why it's useful: CI/CD pipelines often post results as PR comments. A Markdown table renders cleanly in GitHub, GitLab, and Bitbucket PR discussions.
What you'll learn:
- Adding a new output format to the report layer
- Working with
io.Writerfor composable output - Extending CLI flag handling
Hints:
- Create
internal/report/markdown.gowith functions matching the terminal report signatures - The vulnerability table should have columns: Package, Vulnerability ID, Severity, CVSS Score, Fix Version
- Register the format in
internal/cli/scan.go,vuln.go, andcheck.goalongsideterminalandjson
Test it works:
Run bomber vuln testdata/go-project --format markdown and verify the output renders correctly when pasted into a GitHub issue.
Challenge 3: Add --direct-only Flag
What to build: A flag that filters scan and vulnerability results to only show direct dependencies, ignoring transitives.
Why it's useful: When triaging vulnerabilities, direct dependencies are actionable (you can bump the version in your manifest). Transitive vulnerabilities require waiting for your direct dependency to update, which is a different workflow.
What you'll learn:
- Adding persistent flags to Cobra commands
- Filtering
ScanResultandVulnReportbased onPackage.Direct
Hints:
- Add the flag in
internal/cli/root.gowithrootCmd.PersistentFlags().BoolVar - Filter in
queryVulnsbefore sending to the API: only include packages wherepkg.Direct == true - Also filter
ScanResultfor the scan summary counts
Test it works:
Compare output of bomber vuln testdata/go-project with and without --direct-only. The direct-only version should show fewer packages.
Intermediate Challenges
Challenge 4: Add Rust Ecosystem Support
What to build: A parser for Rust projects that reads Cargo.toml and Cargo.lock.
Real world application: Rust is growing fast in systems programming and security tooling. Cargo.lock has a well-defined format with explicit dependency sections.
What you'll learn:
- Implementing the
DependencyParserinterface for a new ecosystem - TOML parsing for Cargo's format (different from Python's pyproject.toml)
- PURL construction for the
cargotype
Implementation approach:
-
Add
EcosystemRustto theEcosystemenum inpkg/types/types.gowith aString()case returning"rust" -
Create
internal/parser/cargo.go:Detect(): check forCargo.tomlParse(): readCargo.tomlfor project name/version and direct dependencies, readCargo.lockfor resolved versions and the full dependency tree- PURLs follow the format
pkg:cargo/serde@1.0.200
-
Register in
internal/parser/registry.go -
Create test fixtures in
testdata/rust-project/with aCargo.tomlandCargo.lock
Hints:
- Cargo.lock has
[[package]]TOML arrays withname,version,source, andchecksumfields - Dependencies in Cargo.lock are listed as
dependencies = ["dep1", "dep2 0.3.0 (registry+...)"] - The tricky part is parsing the dependency format, which can include version and source constraints
Test it works:
Create a test Rust project fixture and run bomber scan testdata/rust-project. Verify the package count and ecosystem detection.
Challenge 5: Add SARIF Output for CI Integration
What to build: A --format sarif option that outputs vulnerability findings in SARIF v2.1.0 format for integration with GitHub Code Scanning, Azure DevOps, and other CI platforms.
Real world application: GitHub Actions can ingest SARIF files and display findings inline on pull requests. This is how tools like CodeQL, Snyk, and Trivy surface results in the GitHub UI.
What you'll learn:
- SARIF specification structure (runs, results, rules, locations)
- Building complex JSON documents with nested structs
- CI/CD integration patterns
Implementation approach:
-
Create
internal/report/sarif.gowith SARIF v2.1.0 types:sarifLogwith$schema,version,runs- Each vulnerability becomes a
resultwith aruleId,message, andlevel - Map CVSS severity to SARIF levels: CRITICAL/HIGH = "error", MEDIUM = "warning", LOW = "note"
-
Map Bomber data to SARIF fields:
- Rule ID = vulnerability ID (e.g.,
CVE-2023-44487) - Rule shortDescription = vulnerability summary
- Result message = package PURL + fix version if available
- Level = severity mapping
- Rule ID = vulnerability ID (e.g.,
-
Add format handling in the CLI commands
Hints:
- The SARIF schema URL is
https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json - GitHub Actions uses
github/codeql-action/upload-sarif@v3to ingest SARIF files - Look at the secrets-scanner project's
internal/reporter/sarif.gofor a working SARIF implementation in the same codebase
Test it works:
Run bomber vuln testdata/go-project --format sarif | jq . and verify it's valid SARIF. Upload it to GitHub Code Scanning in a test repo.
Challenge 6: Add License Detection
What to build: Extract license information from dependencies and add it to the SBOM output.
Real world application: SBOMs aren't just for vulnerability tracking. License compliance is the other major use case. Organizations need to know if they're using GPL-licensed code in proprietary software.
What you'll learn:
- SPDX license expression syntax
- Extending the
Packagetype with new fields - Extracting license data from ecosystem-specific sources
Implementation approach:
-
Add
License stringfield totypes.Package -
Go parser: Extract license from
go.sumor by runninggo list -m -jsonwhich includes aDirfield you can scan for LICENSE files -
Node parser: Read the
licensefield frompackage.jsonentries inpnpm-lock.yaml -
Python parser: Read the
licensefield from package metadata (this may require querying PyPI or readingdist-info) -
Update SBOM generators:
- SPDX: set
licenseConcludedandlicenseDeclaredfields - CycloneDX: add
licensesarray to components
- SPDX: set
Hints:
- Start with just the Node parser, which has license info readily available in
package.json - SPDX license identifiers are standardized: "MIT", "Apache-2.0", "GPL-3.0-only"
- For packages where license can't be determined, use "NOASSERTION" (SPDX) or omit the field (CycloneDX)
Advanced Challenges
Challenge 7: Add Reachability Analysis
What to build: Determine whether a vulnerable function is actually called by the project, not just imported as a dependency.
Why this is hard: A project can depend on golang.org/x/net without ever calling the vulnerable HTTP/2 functions. Knowing the dependency is present is useful; knowing whether the vulnerable code path is reachable is much more actionable.
What you'll learn:
- Static analysis with Go's
go/astandgo/typespackages - Call graph construction
- False positive reduction in vulnerability scanning
Architecture changes needed:
┌───────────────────┐ ┌───────────────────┐
│ Vulnerability │ │ Call Graph │
│ Match (existing) │────▶│ Analysis (new) │
│ pkg + vuln ID │ │ symbols reachable? │
└───────────────────┘ └───────────┬────────┘
│
▼
┌───────────────────┐
│ Enriched Match │
│ + reachable: bool │
└───────────────────┘
Implementation steps:
-
Research phase: Study Go's
golang.org/x/tools/go/callgraphpackage and thevulncheckapproach used bygovulncheck -
Design phase: Decide whether to do import-level analysis (which packages are imported) or symbol-level analysis (which functions are called). Import-level is simpler but less precise.
-
Implementation phase:
- Parse Go source files with
go/ast - Build an import graph
- Cross-reference with vulnerability affected symbols
- Add
Reachable booltoVulnMatch
- Parse Go source files with
-
Testing phase: Create test fixtures with both reachable and unreachable vulnerable imports
Gotchas:
- Only feasible for Go projects (Go's tooling supports this analysis). For Node and Python, import-level analysis is the practical ceiling.
- Reflection and dynamic dispatch defeat static analysis. You'll need to flag these cases as "unknown reachability" rather than "not reachable."
Challenge 8: Build a Dependency Diff Tool
What to build: A bomber diff command that compares two SBOMs and shows what changed: added packages, removed packages, version changes, new vulnerabilities, resolved vulnerabilities.
Why this is hard: Diffing SBOMs requires normalizing identifiers across formats, handling renames and version bumps, and presenting changes in a way that's actionable.
What you'll learn:
- SBOM parsing (the reverse of generation)
- Set operations on dependency data
- Changelog presentation for security context
Implementation steps:
-
Create
internal/diff/diff.gowith types:Added []Package Removed []Package Changed []PackageChange (old version → new version) NewVulns []VulnMatch ResolvedVulns []VulnMatch -
Add
bomber diff old.json new.jsoncommand -
Handle both SPDX and CycloneDX input formats by detecting the format from the JSON structure
-
Run vulnerability scans on both the old and new package sets to compute the vulnerability delta
Gotchas:
- PURLs without versions (possible in fallback parsing) make diffing ambiguous
- A package can be "changed" in one ecosystem and "removed + added" in another if it moved between lockfiles
Challenge 9: Add GitHub Actions Integration
What to build: A complete GitHub Actions workflow that runs Bomber on every PR, caches results between runs, posts a comment with the vulnerability summary, and blocks merging if the policy fails.
Why this is hard: CI integration involves environment detection, artifact caching, GitHub API interaction, and idempotent comment posting (update existing comment rather than creating duplicates).
What you'll learn:
- GitHub Actions workflow syntax and caching
- GitHub REST API for PR comments
- CI/CD security scanning patterns used in production
Implementation steps:
-
Create
.github/workflows/sbom-check.yml:- Trigger on
pull_request - Install Bomber (use the install script or
go install) - Run
bomber check . --policy policy.yaml --format json - Cache
~/.bomber/cache.dbbetween runs
- Trigger on
-
Create a comment script that reads Bomber's JSON output and formats it as a Markdown PR comment using
gh api -
Handle idempotent comments: search for existing Bomber comments on the PR and update them instead of creating new ones
-
Add status check integration: use
bomber checkexit code to pass/fail the check
Gotchas:
- The cache path needs to be relative or configurable for CI caching
- PR comment permissions require
pull-requests: writein the workflow - Rate limits on the GitHub API need handling for monorepos with many PRs
Expert Challenges
Challenge 10: Build a Vulnerability Database Mirror
What to build: A local mirror of the OSV database that Bomber queries instead of the live API. This eliminates network dependency and enables air-gapped environments.
Prerequisites: You should have completed the cache challenge and understand SQLite schema design.
What you'll learn:
- Database synchronization patterns
- Bulk data ingestion and indexing
- Offline security scanning (required in air-gapped government/defense environments)
Planning this feature:
Before you code, think through:
- How do you handle initial sync vs incremental updates?
- What's the storage footprint? (OSV has ~200,000+ vulnerability records)
- How do you query the local database with the same PURL-based interface?
Implementation phases:
Phase 1: Database schema
- Design SQLite tables for vulnerabilities, affected packages, and version ranges
- Index by PURL prefix (ecosystem type) for fast lookups
Phase 2: Sync engine
- Use OSV's bulk export (Google Cloud Storage buckets) or the list endpoint
- Implement incremental sync based on last-modified timestamps
Phase 3: Local query client
- Implement
vuln.Clientinterface backed by the local database - Replace HTTP queries with SQL queries
- Handle version range matching locally
Phase 4: CLI integration
- Add
bomber mirror synccommand to trigger sync - Add
--offlineflag to use only the local mirror - Fall back to API if mirror is stale
Success criteria:
- Initial sync completes in under 10 minutes
- Incremental sync completes in under 30 seconds
- Query results match live API results for the test fixtures
- Works without network access after initial sync
- Storage is under 500MB for the full OSV database
Performance Challenges
Challenge: Handle 10,000-Dependency Monorepos
The goal: Make Bomber handle a monorepo with 10,000+ total dependencies without running out of memory or taking more than 60 seconds (excluding API calls).
Current bottleneck: The dependency graph stores all packages in memory. SBOM generation serializes the entire graph to JSON. For very large projects, both the graph and the JSON output can be large.
Optimization approaches:
Approach 1: Streaming SBOM generation
- Instead of building the full JSON document in memory, stream SPDX/CycloneDX output using
json.Encoder - Gain: Constant memory for SBOM generation regardless of graph size
- Tradeoff: Can't pretty-print with
json.MarshalIndent(need custom formatting)
Approach 2: Parallel parsing
- Run ecosystem parsers concurrently with
errgroup - Gain: A monorepo with Go, Node, and Python projects scans 3x faster
- Tradeoff: Need to synchronize graph accumulation
Benchmark it:
time bomber scan ./large-monorepo
time bomber generate ./large-monorepo --sbom-format cyclonedx > /dev/null
Target metrics:
- Scan phase: < 5 seconds for 10,000 packages
- SBOM generation: < 10 seconds, < 200MB RSS
Challenge: Reduce Binary Size
The goal: Get the Bomber binary under 5MB (current is larger due to the SQLite dependency).
Optimization approaches:
- Use
go build -ldflags="-s -w"(already in the Justfile, strips debug info and symbol table) - Consider
upxcompression for distribution - Profile the binary with
go tool nmto identify large dependencies - Evaluate whether
modernc.org/sqlitecan be replaced with a lighter cache (e.g., bbolt) if SQLite features aren't needed
Security Challenges
Challenge: Add SBOM Signing
What to implement: Cryptographic signing of generated SBOMs so consumers can verify they haven't been tampered with.
Threat model: An attacker who can modify SBOMs in transit or at rest could hide vulnerabilities or add fake clean reports. Signing prevents this.
Implementation:
- Use
cosign(Sigstore) ornotation(Notary v2) for keyless signing - Add
--signflag to the generate command - Attach the signature as a separate
.sigfile or embed it in the SBOM metadata - Add
bomber verify <sbom-file>command to validate signatures
Testing the security:
- Modify a signed SBOM and verify that
bomber verifydetects the tampering - Verify that signing works with both SPDX and CycloneDX output
Challenge: Add VEX (Vulnerability Exploitability eXchange) Support
What to implement: VEX document generation that states whether vulnerabilities in an SBOM are actually exploitable in your specific context.
Real world application: A vulnerability in a dependency you import but never call its affected function is "not affected." VEX lets you formally state this, reducing noise for downstream consumers of your SBOM.
Implementation:
- Add
bomber vex ./projectcommand - For each vulnerability found, prompt or accept a status: "not_affected", "affected", "fixed", "under_investigation"
- Output VEX in CycloneDX VEX or OpenVEX format
- Store VEX statements in a local file (
.bomber/vex.json) so they persist across scans
Real World Integration Challenges
Integrate with GitHub Dependency Graph
The goal: Upload Bomber's dependency data to GitHub's Dependency Graph API so it appears in the repository's "Insights > Dependency graph" tab.
What you'll need:
- GitHub API token with
reposcope - Understanding of GitHub's Dependency Submission API
- PURL-to-GitHub ecosystem mapping
Implementation plan:
- Generate a Dependency Snapshot from Bomber's scan results
- POST to
https://api.github.com/repos/{owner}/{repo}/dependency-graph/snapshots - Handle the response and report success/failure
Watch out for:
- GitHub's snapshot format has specific requirements for manifest file paths
- Rate limits on the Dependency Submission API
Integrate with Dependency-Track
The goal: Upload Bomber-generated SBOMs to an OWASP Dependency-Track instance for continuous monitoring.
What you'll need:
- A running Dependency-Track instance (Docker:
docker run -p 8080:8080 dependencytrack/apiserver) - API key from Dependency-Track settings
Implementation plan:
- Add
bomber upload --server <url> --project <name>command - Generate CycloneDX SBOM (Dependency-Track's preferred format)
- POST to Dependency-Track's BOM upload API
- Report the project URL for the uploaded SBOM
Challenge Completion
Track your progress:
- Easy: Blocked packages policy rule
- Easy: Markdown report output
- Easy: Direct-only flag
- Intermediate: Rust ecosystem support
- Intermediate: SARIF output
- Intermediate: License detection
- Advanced: Reachability analysis
- Advanced: Dependency diff tool
- Advanced: GitHub Actions integration
- Expert: Vulnerability database mirror
- Performance: 10,000-dependency monorepos
- Performance: Binary size reduction
- Security: SBOM signing
- Security: VEX support
- Integration: GitHub Dependency Graph
- Integration: Dependency-Track
Completed all of them? You've built a production-grade supply chain security tool. Consider contributing your extensions back to the project or building a standalone tool based on what you've learned.