Cybersecurity-Projects/PROJECTS/intermediate/sbom-generator-vulnerabilit.../internal/parser/gomod.go

228 lines
4.7 KiB
Go

// ©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
}