Skip to content

Commit

Permalink
feat: dependency graph support (#1) �
Browse files Browse the repository at this point in the history
  • Loading branch information
nscuro committed Mar 10, 2021
1 parent 01a4abb commit d0ecdb7
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 6 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ build:
.PHONY: build

test:
go test -v -cover
go test -v -cover ./...
.PHONY: test

clean:
go clean
go clean ./...
.PHONY: clean

bom:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ go 1.16
require (
github.com/CycloneDX/cyclonedx-go v0.1.0
github.com/google/uuid v1.2.0
github.com/stretchr/testify v1.7.0
golang.org/x/mod v0.4.1
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
60 changes: 57 additions & 3 deletions internal/gomod/gomod.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gomod

import (
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -46,8 +47,52 @@ func (m Module) Hashes() ([]cdx.Hash, error) {
}, nil
}

func (m Module) ModuleGraph() (map[string][]string, error) {
cmd := exec.Command("go", "mod", "graph")
cmd.Dir = m.Dir

output, err := cmd.Output()
if err != nil {
return nil, err
}

return m.parseModuleGraph(bytes.NewReader(output))
}

func (m Module) parseModuleGraph(reader io.Reader) (map[string][]string, error) {
graph := make(map[string][]string)

scanner := bufio.NewScanner(reader)
for scanner.Scan() {
parts := strings.SplitN(scanner.Text(), " ", 2)

dependant := parts[0]
if dependant == m.Path {
// The main module has no version in the module graph
dependant = m.PackageURL()
} else {
dependant = coordinatesToPURL(dependant)
}
dependency := coordinatesToPURL(parts[1])

dependencies, ok := graph[dependant]
if !ok {
dependencies = []string{dependency}
} else {
dependencies = append(dependencies, dependency)
}
graph[dependant] = dependencies

// For a complete graph, dependencies must be included as dependants as well
if _, ok := graph[dependency]; !ok {
graph[dependency] = make([]string, 0)
}
}
return graph, nil
}

func (m Module) PackageURL() string {
return fmt.Sprintf("pkg:golang/%s@%s", m.Path, m.Version)
return coordinatesToPURL(m.Path + "@" + m.Version)
}

func GetModules(path string) ([]Module, error) {
Expand All @@ -63,9 +108,14 @@ func GetModules(path string) ([]Module, error) {
return nil, err
}

// Output is not a JSON array, so we have to parse one object after another
return parseModules(bytes.NewReader(output))
}

func parseModules(reader io.Reader) ([]Module, error) {
modules := make([]Module, 0)
jsonDecoder := json.NewDecoder(bytes.NewReader(output))
jsonDecoder := json.NewDecoder(reader)

// Output is not a JSON array, so we have to parse one object after another
for {
var mod Module
if err := jsonDecoder.Decode(&mod); err != nil {
Expand Down Expand Up @@ -131,3 +181,7 @@ func GetVersionFromTag(path string) (string, error) {

return strings.TrimSpace(string(output)), nil
}

func coordinatesToPURL(coordinates string) string {
return "pkg:golang/" + coordinates
}
84 changes: 84 additions & 0 deletions internal/gomod/gomod_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package gomod

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestModule_ParseModuleGraph(t *testing.T) {
rawGraph := `github.com/CycloneDX/cyclonedx-go github.com/bradleyjkemp/cupaloy/v2@v2.6.0
github.com/CycloneDX/cyclonedx-go github.com/stretchr/testify@v1.7.0
github.com/bradleyjkemp/cupaloy/v2@v2.6.0 github.com/davecgh/go-spew@v1.1.1
github.com/bradleyjkemp/cupaloy/v2@v2.6.0 github.com/pmezard/go-difflib@v1.0.0
github.com/bradleyjkemp/cupaloy/v2@v2.6.0 github.com/stretchr/objx@v0.1.1
github.com/bradleyjkemp/cupaloy/v2@v2.6.0 github.com/stretchr/testify@v1.6.1
github.com/stretchr/testify@v1.7.0 github.com/davecgh/go-spew@v1.1.0
github.com/stretchr/testify@v1.7.0 github.com/pmezard/go-difflib@v1.0.0
github.com/stretchr/testify@v1.7.0 github.com/stretchr/objx@v0.1.0
github.com/stretchr/testify@v1.7.0 gopkg.in/yaml.v3@v3.0.0-20200313102051-9f266ea9e77c
github.com/stretchr/testify@v1.6.1 github.com/davecgh/go-spew@v1.1.0
github.com/stretchr/testify@v1.6.1 github.com/pmezard/go-difflib@v1.0.0
github.com/stretchr/testify@v1.6.1 github.com/stretchr/objx@v0.1.0
github.com/stretchr/testify@v1.6.1 gopkg.in/yaml.v3@v3.0.0-20200313102051-9f266ea9e77c
gopkg.in/yaml.v3@v3.0.0-20200313102051-9f266ea9e77c gopkg.in/check.v1@v0.0.0-20161208181325-20d25e280405
`
module := Module{
Path: "github.com/CycloneDX/cyclonedx-go",
Version: "v0.1.0",
}

graph, err := module.parseModuleGraph(strings.NewReader(rawGraph))
require.NoError(t, err)

directDependencies, ok := graph["pkg:golang/github.com/CycloneDX/cyclonedx-go@v0.1.0"]
require.True(t, ok)
assert.Contains(t, directDependencies, "pkg:golang/github.com/bradleyjkemp/cupaloy/v2@v2.6.0")
assert.Contains(t, directDependencies, "pkg:golang/github.com/stretchr/testify@v1.7.0")

transitiveDependencies, ok := graph["pkg:golang/github.com/bradleyjkemp/cupaloy/v2@v2.6.0"]
require.True(t, ok)
assert.Contains(t, transitiveDependencies, "pkg:golang/github.com/davecgh/go-spew@v1.1.1")
assert.Contains(t, transitiveDependencies, "pkg:golang/github.com/pmezard/go-difflib@v1.0.0")
assert.Contains(t, transitiveDependencies, "pkg:golang/github.com/stretchr/objx@v0.1.1")
assert.Contains(t, transitiveDependencies, "pkg:golang/github.com/stretchr/testify@v1.6.1")
}

func TestModule_PackageURL(t *testing.T) {
module := Module{
Path: "github.com/CycloneDX/cyclonedx-go",
Version: "v0.1.0",
}

assert.Equal(t, "pkg:golang/github.com/CycloneDX/cyclonedx-go@v0.1.0", module.PackageURL())
}

func TestParseModules(t *testing.T) {
modulesJSON := `{
"Path": "github.com/CycloneDX/cyclonedx-go",
"Main": true,
"Dir": "/path/to/cyclonedx-go",
"GoMod": "/path/to/cyclonedx-go/go.mod",
"GoVersion": "1.14"
}
{
"Path": "github.com/davecgh/go-spew",
"Version": "v1.1.1",
"Time": "2018-02-21T23:26:28Z",
"Indirect": true,
"Dir": "/path/to/go/pkg/mod/github.com/davecgh/go-spew@v1.1.1",
"GoMod": "/path/to/go/pkg/mod/cache/download/github.com/davecgh/go-spew/@v/v1.1.1.mod"
}`

modules, err := parseModules(strings.NewReader(modulesJSON))
require.NoError(t, err)

assert.Len(t, modules, 2)
assert.Equal(t, "github.com/CycloneDX/cyclonedx-go", modules[0].Path)
assert.True(t, modules[0].Main)
assert.Equal(t, "github.com/davecgh/go-spew", modules[1].Path)
assert.Equal(t, "v1.1.1", modules[1].Version)
assert.False(t, modules[1].Main)
}
28 changes: 27 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ func main() {
}
bom.Components = &components

moduleGraph, err := mainModule.ModuleGraph()
if err != nil {
log.Fatalf("failed to get module graph: %v", err)
}

depGraph := buildDependencyGraph(moduleGraph)
bom.Dependencies = &depGraph

var outputFormat cdx.BOMFileFormat
if useJSON {
outputFormat = cdx.BOMFileFormatJSON
Expand Down Expand Up @@ -160,7 +168,7 @@ func validateArguments() error {
return fmt.Errorf("invalid component type %s. See https://pkg.go.dev/github.com/CycloneDX/cyclonedx-go#ComponentType for options", componentType)
}

// Serial number must be valid UUIDs
// Serial numbers must be valid UUIDs
if !noSerialNumber && serialNumber != "" {
if _, err := uuid.Parse(serialNumber); err != nil {
return fmt.Errorf("invalid serial number: %w", err)
Expand Down Expand Up @@ -236,3 +244,21 @@ func convertToComponent(module gomod.Module) cdx.Component {

return component
}

func buildDependencyGraph(moduleGraph map[string][]string) []cdx.Dependency {
depGraph := make([]cdx.Dependency, 0)

for dependant, dependencies := range moduleGraph {
cdxDependant := cdx.Dependency{Ref: dependant}
cdxDependencies := make([]cdx.Dependency, len(dependencies))
for i := range dependencies {
cdxDependencies[i] = cdx.Dependency{Ref: dependencies[i]}
}
if len(cdxDependencies) > 0 {
cdxDependant.Dependencies = &cdxDependencies
}
depGraph = append(depGraph, cdxDependant)
}

return depGraph
}

0 comments on commit d0ecdb7

Please sign in to comment.