228 lines
4.7 KiB
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
|
|
}
|