// ©AngelaMos | 2026 // gomod.go package parser import ( "bufio" "bytes" "encoding/base64" "encoding/hex" "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/CarterPerez-dev/bomber/pkg/types" ) type GoModParser struct{} func NewGoModParser() *GoModParser { return &GoModParser{} } func (p *GoModParser) Ecosystem() types.Ecosystem { return types.EcosystemGo } func (p *GoModParser) Detect(dir string) bool { _, err := os.Stat(filepath.Join(dir, "go.mod")) return err == nil } func (p *GoModParser) Parse(dir string) (*types.DependencyGraph, error) { modPath := filepath.Join(dir, "go.mod") modFile, err := os.Open(modPath) if err != nil { return nil, fmt.Errorf("open go.mod: %w", err) } defer func() { _ = modFile.Close() }() var moduleName string directDeps := make(map[string]string) indirectDeps := make(map[string]string) scanner := bufio.NewScanner(modFile) inRequire := false for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "module ") { moduleName = strings.TrimPrefix(line, "module ") continue } if line == "require (" { inRequire = true continue } if line == ")" { inRequire = false continue } if inRequire { parts := strings.Fields(line) if len(parts) < 2 { continue } name := parts[0] version := parts[1] if strings.Contains(line, "// indirect") { indirectDeps[name] = version } else { directDeps[name] = version } } } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("read go.mod: %w", err) } root := types.Package{ Name: moduleName, Ecosystem: types.EcosystemGo, PURL: fmt.Sprintf("pkg:golang/%s", moduleName), Direct: true, } graph := types.NewDependencyGraph(root) checksums := parseGoSum(filepath.Join(dir, "go.sum")) for name, version := range directDeps { purl := fmt.Sprintf("pkg:golang/%s@%s", name, version) pkg := types.Package{ Name: name, Version: version, Ecosystem: types.EcosystemGo, PURL: purl, Direct: true, Checksums: checksums[name+"@"+version], } graph.AddPackage(pkg) graph.AddEdge(root.PURL, purl) } for name, version := range indirectDeps { purl := fmt.Sprintf("pkg:golang/%s@%s", name, version) pkg := types.Package{ Name: name, Version: version, Ecosystem: types.EcosystemGo, PURL: purl, Direct: false, Checksums: checksums[name+"@"+version], } graph.AddPackage(pkg) } parseGoModGraph(dir, graph) computeDepthLevels(graph) return graph, nil } func parseGoModGraph(dir string, graph *types.DependencyGraph) { cmd := exec.Command("go", "mod", "graph") cmd.Dir = dir out, err := cmd.Output() if err != nil { return } scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { parts := strings.Fields(scanner.Text()) if len(parts) != 2 { continue } parentPURL := goDepToPURL(parts[0]) childPURL := goDepToPURL(parts[1]) if _, exists := graph.Nodes[childPURL]; !exists { continue } if _, exists := graph.Nodes[parentPURL]; !exists { continue } graph.AddEdge(parentPURL, childPURL) } } func goDepToPURL(dep string) string { if !strings.Contains(dep, "@") { return fmt.Sprintf("pkg:golang/%s", dep) } parts := strings.SplitN(dep, "@", 2) return fmt.Sprintf("pkg:golang/%s@%s", parts[0], parts[1]) } func computeDepthLevels(graph *types.DependencyGraph) { depths := make(map[string]int) depths[graph.Root.PURL] = 0 queue := []string{graph.Root.PURL} for len(queue) > 0 { current := queue[0] queue = queue[1:] currentDepth := depths[current] for _, child := range graph.Edges[current] { if _, visited := depths[child]; !visited { depths[child] = currentDepth + 1 queue = append(queue, child) } } } for purl, depth := range depths { if pkg, ok := graph.Nodes[purl]; ok { pkg.DepthLevel = depth graph.Nodes[purl] = pkg } } } func parseGoSum(path string) map[string][]types.Checksum { checksums := make(map[string][]types.Checksum) f, err := os.Open(path) if err != nil { return checksums } defer func() { _ = f.Close() }() scanner := bufio.NewScanner(f) for scanner.Scan() { parts := strings.Fields(scanner.Text()) if len(parts) != 3 { continue } name := parts[0] version := strings.TrimSuffix(parts[1], "/go.mod") hash := parts[2] if !strings.HasPrefix(hash, "h1:") { continue } raw := strings.TrimPrefix(hash, "h1:") decoded, err := base64.StdEncoding.DecodeString(raw) if err != nil { continue } key := name + "@" + version checksums[key] = append(checksums[key], types.Checksum{ Algorithm: "SHA-256", Value: hex.EncodeToString(decoded), }) } return checksums }