You are extending forgeseal (github.com/sns45/forgeseal), a Go-based supply chain security CLI that generates CycloneDX SBOMs, Sigstore signatures, SLSA provenance, and VEX documents. It currently supports JS/TS (6 lockfile formats) and Python (4 lockfile formats).
You are adding support for Go (go.mod/go.sum), Rust (Cargo.lock), and Java/Gradle (gradle.lockfile) — 3 new parsers, 3 new PURL types, and updates to the SBOM generator, component mapper, and test suite.
Read requirements.md first — it is the authoritative spec for this task.
Use write-plan followed by execute-plan to implement this in phases. The plan should have these phases:
Plan name: forgeseal-gomod
-
Add
TypeGoMod LockfileType = "gomod"tointernal/lockfile/model.go -
Create
internal/lockfile/gomod.go:GoModParserstruct implementingParserinterfaceFilenames()returns["go.mod"]Parse()readsgo.modfrom the reader, then attempts to readgo.sumfrom the same directory by implementingFileParserinterface instead (useParseFileto get the directory path)- Parse
requireblocks (both parenthesized and single-line), handling// indirectcomments - Parse
replacedirectives and apply them to the dependency list - Parse
go.sumforh1:integrity hashes (use the non-/go.modentries) - Skip workspace members / the module itself
-
Create test fixture
testdata/lockfiles/gomod/go.modwith:module example.com/myprojectgo 1.22- A
requireblock with ~8–10 deps including indirect ones - At least one
replacedirective (e.g.,replace golang.org/x/old => golang.org/x/new v0.5.0) - At least one
/v2major version module - Use real-world module paths (github.com/gorilla/mux, golang.org/x/sys, etc.) with plausible versions
-
Create matching
testdata/lockfiles/gomod/go.sumwithh1:hashes for each module -
Create
internal/lockfile/gomod_test.go:TestGoModParser: parse fixture, verify package count, names, versions, replace resolution, hash extractionTestGoModParserReplace: verify replace directives are resolved correctlyTestParseGoModLine: unit test for line parsing helper
-
Add
BuildGolangPURL(name, version string) stringtointernal/sbom/purl.go:- Use
packageurl.NewPackageURL("golang", namespace, name, version, nil, "") - Split module path on last
/for namespace vs name - Add test cases to
purl_test.go
- Use
-
Update
internal/sbom/cyclonedx.gomapComponent():- For
"golang"ecosystem: external ref URL =https://pkg.go.dev/<module> - Hash parsing for
h1:<base64>format →cdx.HashAlgoSHA256
- For
-
Update
internal/sbom/generator.go:- Add
TypeGoModcase toecosystemFromLockfileType()returning"golang" - Add
readGoModInfo()to extract module name fromgo.mod - Wire into
Generate()metadata chain
- Add
-
Update
internal/sbom/purl.goBuildPURL()dispatcher:- Add
case "golang": return BuildGolangPURL(name, version)
- Add
-
Register
&GoModParser{}ininternal/lockfile/parser.goregistry (after Python parsers) -
Run
go test ./internal/lockfile/ -run TestGoModandgo test ./internal/sbom/ -run TestSnapshots/gomod -updateto generate the snapshot -
Run
go test ./...to verify nothing is broken
Plan name: forgeseal-cargo
-
Add
TypeCargoLock LockfileType = "cargo"tointernal/lockfile/model.go -
Create
internal/lockfile/cargo.go:CargoParserstruct implementingParserFilenames()returns["Cargo.lock"]- Parse TOML using
pelletier/go-toml/v2with map-based decoding (same pattern asuv.go) - Extract
[[package]]array entries:name,version,source,checksum,dependencies - Skip packages without
sourcefield (workspace members) - Parse dependency strings:
"name","name version","name version (source)" - Store
checksumassha256:<hex>inPackage.Integrity
-
Create test fixture
testdata/lockfiles/cargo/Cargo.lockwith:version = 3header- A workspace root package (no
source) - ~8–10 registry packages with checksums
- At least one package with multiple versions
- Dependency strings in various formats
- Use real crate names (serde, tokio, rand, etc.)
-
Create
internal/lockfile/cargo_test.go:TestCargoParser: parse fixture, verify count, names, versions, checksums, workspace root skippingTestParseCargoDepString: unit test for dependency string parsing
-
Add
BuildCargoPURL(name, version string) stringtointernal/sbom/purl.go:packageurl.NewPackageURL("cargo", "", name, version, nil, "")- Add test cases
-
Update
mapComponent()for"cargo": external ref URL =https://crates.io/crates/<n> -
Update
ecosystemFromLockfileType():TypeCargoLock → "cargo" -
Update
BuildPURL()dispatcher:case "cargo": return BuildCargoPURL(name, version) -
Add
readCargoTomlInfo()to extract[package].nameand[package].versionfromCargo.tomlif present -
Register
&CargoParser{}in parser registry -
Generate snapshot and run full test suite
Plan name: forgeseal-gradle
-
Add
TypeGradleLock LockfileType = "gradle"tointernal/lockfile/model.go -
Create
internal/lockfile/gradle.go:GradleParserstruct implementingParserFilenames()returns["gradle.lockfile"]- Line-oriented parsing: skip
#comments and blank lines - Parse
<group>:<artifact>:<version>=<configurations>format - Store
Package.Nameas"group:artifact"(colon-joined for PURL splitting later) - Dev detection: if configurations CSV contains ONLY
test*prefixed configs, markDev = true
-
Create test fixture
testdata/lockfiles/gradle/gradle.lockfilewith:- Standard header comment
- ~8–10 dependencies across various configurations
- At least 2 test-only dependencies
- Use real Maven coordinates (org.springframework:spring-core, com.google.guava:guava, etc.)
-
Create
internal/lockfile/gradle_test.go:TestGradleParser: parse fixture, verify count, names (group:artifact format), versions, dev detectionTestGradleDevDetection: verify configuration-based dev classification
-
Add
BuildMavenPURL(name, version string) stringtointernal/sbom/purl.go:- Split
nameon:→group(namespace) andartifact(name) packageurl.NewPackageURL("maven", group, artifact, version, nil, "")- Add test cases
- Split
-
Update
mapComponent()for"maven":- External ref URL =
https://central.sonatype.com/artifact/<group>/<artifact> - Use
artifactportion as display name incomp.Name
- External ref URL =
-
Update
ecosystemFromLockfileType():TypeGradleLock → "maven" -
Update
BuildPURL()dispatcher:case "maven": return BuildMavenPURL(name, version) -
Register
&GradleParser{}in parser registry -
Generate snapshot and run full test suite
Plan name: forgeseal-integration
-
Update
Detect()error message inparser.goto includego.mod, Cargo.lock, gradle.lockfile -
Update
internal/sbom/generator_test.go:- Add
TestGeneratorGenerateGowith Go lockfile result - Add
TestGeneratorGenerateRustwith Cargo lockfile result - Add
TestGeneratorGenerateGradlewith Gradle lockfile result - Verify PURL prefixes (
pkg:golang/,pkg:cargo/,pkg:maven/)
- Add
-
Update
ecosystemFromLockfileTypetest ingenerator_test.goto cover all new types -
Run
go test -race -count=1 ./...— everything must pass -
Update
README.md:- Add Go, Rust, Gradle rows to Supported Lockfiles table
- Update description and feature summary
- Add Go/Rust/Gradle quick start examples
- Update Architecture section with new parser count
- Update PURL construction docs
-
Update
action.ymldescription to mention Go, Rust, Java -
Update
.goreleaser.yamlbrew description -
Update
rootCmd.LongandrootCmd.Shortininternal/cli/root.go -
Final
go test -race -count=1 ./...— confirm green
These are the patterns already established in the codebase. Follow them exactly:
type XxxParser struct{}
func (p *XxxParser) Type() LockfileType { return TypeXxx }
func (p *XxxParser) Filenames() []string { return []string{"filename"} }
func (p *XxxParser) Parse(ctx context.Context, r io.Reader) (*LockfileResult, error) { ... }Use map-based decoding like uv.go and pdm.go:
var raw map[string]any
if err := toml.Unmarshal(data, &raw); err != nil { ... }
packageList, ok := raw["package"].([]any)func BuildXxxPURL(name, version string) string {
purl := packageurl.NewPackageURL("type", namespace, name, version, nil, "")
return purl.ToString()
}func TestXxxParser(t *testing.T) {
f, err := os.Open("../../testdata/lockfiles/xxx/filename")
// ... parse, verify Type, verify package count
found := make(map[string]Package)
for _, pkg := range result.Packages { found[pkg.Name] = pkg }
// ... assert specific packages by name, version, integrity, deps, dev flag
}New testdata directories are auto-discovered by discoverSnapshotCases() in snapshot_test.go. Just create the fixture and run with -update.
-
ZERO new
go.moddependencies —pelletier/go-toml/v2is already available for Cargo.lock. Go.mod and gradle.lockfile need only string parsing. -
Never shell out to
go,cargo, orgradle. Parse lockfiles only. -
Test fixtures must be vendored in
testdata/. No network fetches during tests. -
Preserve backward compatibility — existing JS/TS and Python tests must continue passing unchanged.
-
Maintain detection priority — JS/TS > Python > Go > Rust > Java. A project won't have multiple ecosystems, but the priority exists for mixed-project edge cases.
-
GoModParser needs FileParser interface — because it reads
go.sumfrom the same directory asgo.mod. ImplementParseFile(ctx, path)which readsgo.modfrom the reader AND readsgo.sumby deriving the directory from the path. TheParse(ctx, reader)method should work without hashes (for streaming/testing).
After each phase, verify:
-
go build ./...succeeds -
go vet ./...is clean -
go test -race -count=1 ./...passes (ALL tests, not just new ones) - Snapshot test generates valid CycloneDX JSON
- PURLs match the spec format exactly
- No new entries in
go.mod/go.sum(no new dependencies)
After Phase 4 (final):
- Full test suite passes
-
forgeseal sbom --dir testdata/lockfiles/gomodproduces valid SBOM -
forgeseal sbom --dir testdata/lockfiles/cargoproduces valid SBOM -
forgeseal sbom --dir testdata/lockfiles/gradleproduces valid SBOM - README is updated
-
go test -race -count=1 ./...is green
Run each phase sequentially:
write-plan forgeseal-gomod
execute-plan forgeseal-gomod
write-plan forgeseal-cargo
execute-plan forgeseal-cargo
write-plan forgeseal-gradle
execute-plan forgeseal-gradle
write-plan forgeseal-integration
execute-plan forgeseal-integration
Each phase is self-contained: implement, test, verify green. If a phase fails tests, fix before moving to the next.