mirror of
https://github.com/safedep/vet.git
synced 2025-12-10 00:22:08 -06:00
Merge pull request #187 from safedep/feat/npm-graph-parser
feat: Add support for npm Dependency Graph
This commit is contained in:
commit
774323c28f
2
go.mod
2
go.mod
@ -15,7 +15,7 @@ require (
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.9
|
||||
github.com/kubescape/go-git-url v0.0.25
|
||||
github.com/package-url/packageurl-go v0.1.2
|
||||
github.com/safedep/dry v0.0.0-20231024121814-ee8dd6ec7d93
|
||||
github.com/safedep/dry v0.0.0-20240110142304-5970e3335464
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8
|
||||
github.com/spdx/tools-golang v0.5.3
|
||||
|
||||
4
go.sum
4
go.sum
@ -222,8 +222,8 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/safedep/dry v0.0.0-20231024121814-ee8dd6ec7d93 h1:SLJTcy8drjRSTg8T8yEI7RV0Fhz4U183s8VVB1fzlgk=
|
||||
github.com/safedep/dry v0.0.0-20231024121814-ee8dd6ec7d93/go.mod h1:2XNr8V9meIULvK/pazBdD/KSqB5hsC6XTcnygwuQg4w=
|
||||
github.com/safedep/dry v0.0.0-20240110142304-5970e3335464 h1:H0JZ9xOrJ9VTA4G4TFOAy6+P9hJ7QqAlIBSBdf5uU8c=
|
||||
github.com/safedep/dry v0.0.0-20240110142304-5970e3335464/go.mod h1:2XNr8V9meIULvK/pazBdD/KSqB5hsC6XTcnygwuQg4w=
|
||||
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
|
||||
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
|
||||
|
||||
20
main.go
20
main.go
@ -74,16 +74,18 @@ func main() {
|
||||
}
|
||||
|
||||
func loadExceptions() {
|
||||
if globalExceptionsFile != "" {
|
||||
loader, err := exceptions.NewExceptionsFileLoader(globalExceptionsFile)
|
||||
if err != nil {
|
||||
logger.Fatalf("Exceptions loader: %v", err)
|
||||
}
|
||||
if globalExceptionsFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err = exceptions.Load(loader)
|
||||
if err != nil {
|
||||
logger.Fatalf("Exceptions loader: %v", err)
|
||||
}
|
||||
loader, err := exceptions.NewExceptionsFileLoader(globalExceptionsFile)
|
||||
if err != nil {
|
||||
logger.Fatalf("Exceptions loader: %v", err)
|
||||
}
|
||||
|
||||
err = exceptions.Load(loader)
|
||||
if err != nil {
|
||||
logger.Fatalf("Exceptions loader: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
|
||||
jsonreportspec "github.com/safedep/vet/gen/jsonreport"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/safedep/vet/pkg/common/utils"
|
||||
"github.com/safedep/vet/pkg/models"
|
||||
"github.com/safedep/vet/pkg/readers"
|
||||
)
|
||||
@ -23,6 +24,7 @@ type npmPackageLockPackage struct {
|
||||
Integrity string `json:"integrity"`
|
||||
Dev bool `json:"dev"`
|
||||
Optional bool `json:"optional"`
|
||||
Link bool `json:"link"`
|
||||
}
|
||||
|
||||
// https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json
|
||||
@ -90,6 +92,12 @@ func (npm *npmLockfilePoisoningAnalyzer) Analyze(manifest *models.PackageManifes
|
||||
continue
|
||||
}
|
||||
|
||||
if lockfilePackage.Link {
|
||||
logger.Debugf("npmLockfilePoisoningAnalyzer: Skipping linked package [%s] for [%s]",
|
||||
path, lockfilePackage.Resolved)
|
||||
continue
|
||||
}
|
||||
|
||||
packageName := npmNodeModulesPackagePathToName(path)
|
||||
if packageName == "" {
|
||||
logger.Warnf("npmLockfilePoisoningAnalyzer: Failed to extract package name from path %s", path)
|
||||
@ -229,19 +237,7 @@ func npmIsTrustedSource(sourceUrl string, trusteUrls []string) bool {
|
||||
|
||||
// Extract the package name from the node_modules filesystem path
|
||||
func npmNodeModulesPackagePathToName(path string) string {
|
||||
// Extract the package name from the node_modules filesystem path
|
||||
// Example: node_modules/express -> express
|
||||
// Example: node_modules/@angular/core -> @angular/core
|
||||
// Example: node_modules/@angular/core/node_modules/express -> express
|
||||
// Example: node_modules/@angular/core/node_modules/@angular/common -> @angular/common
|
||||
|
||||
for i := len(path) - 1; i >= 0; i-- {
|
||||
if (len(path[i:]) > 13) && (path[i:i+13] == "node_modules/") {
|
||||
return path[i+13:]
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return utils.NpmNodeModulesPackagePathToName(path)
|
||||
}
|
||||
|
||||
// Test if URL follows the pkg name path convention as per NPM package registry
|
||||
|
||||
@ -83,47 +83,6 @@ func TestNpmIsTrustedSource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNpmNodeModulesPackagePathToName(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"package name is extracted from path",
|
||||
"/a/b/c/node_modules/package-name",
|
||||
"package-name",
|
||||
},
|
||||
{
|
||||
"node_modules relative",
|
||||
"node_modules/express",
|
||||
"express",
|
||||
},
|
||||
{
|
||||
"node_modules relative scoped name",
|
||||
"node_modules/@angular/core",
|
||||
"@angular/core",
|
||||
},
|
||||
{
|
||||
"nested node_modules relative",
|
||||
"node_modules/@angular/core/node_modules/express",
|
||||
"express",
|
||||
},
|
||||
{
|
||||
"nested node_modules relative scoped name",
|
||||
"node_modules/@angular/core/node_modules/@angular/common",
|
||||
"@angular/common",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := npmNodeModulesPackagePathToName(test.path)
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNpmIsUrlFollowsPathConvention(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
@ -52,6 +52,8 @@ func purlBuildLockfilePackageName(ecosystem lockfile.Ecosystem, group, name stri
|
||||
switch ecosystem {
|
||||
case lockfile.GoEcosystem, lockfile.NpmEcosystem:
|
||||
return fmt.Sprintf("%s/%s", group, name)
|
||||
case lockfile.MavenEcosystem:
|
||||
return fmt.Sprintf("%s:%s", group, name)
|
||||
default:
|
||||
return name
|
||||
}
|
||||
|
||||
28
pkg/common/utils/graph.go
Normal file
28
pkg/common/utils/graph.go
Normal file
@ -0,0 +1,28 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/safedep/dry/semver"
|
||||
"github.com/safedep/vet/pkg/models"
|
||||
)
|
||||
|
||||
func FindDependencyGraphNodeBySemverRange(graph *models.DependencyGraph[*models.Package],
|
||||
name string, rangeStr string) *models.DependencyGraphNode[*models.Package] {
|
||||
for _, node := range graph.GetNodes() {
|
||||
if !strings.EqualFold(node.Data.GetName(), name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Exact version match
|
||||
if node.Data.GetVersion() == rangeStr {
|
||||
return node
|
||||
}
|
||||
|
||||
if semver.IsVersionInRange(node.Data.GetVersion(), rangeStr) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
18
pkg/common/utils/npm.go
Normal file
18
pkg/common/utils/npm.go
Normal file
@ -0,0 +1,18 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Re-use from: https://github.com/google/osv-scanner/blob/main/pkg/lockfile/parse-npm-lock.go#L128
|
||||
func NpmNodeModulesPackagePathToName(name string) string {
|
||||
maybeScope := path.Base(path.Dir(name))
|
||||
pkgName := path.Base(name)
|
||||
|
||||
if strings.HasPrefix(maybeScope, "@") {
|
||||
pkgName = maybeScope + "/" + pkgName
|
||||
}
|
||||
|
||||
return pkgName
|
||||
}
|
||||
58
pkg/common/utils/npm_test.go
Normal file
58
pkg/common/utils/npm_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNpmNodeModulesPackagePathToName(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"package name is extracted from path",
|
||||
"/a/b/c/node_modules/package-name",
|
||||
"package-name",
|
||||
},
|
||||
{
|
||||
"node_modules relative",
|
||||
"node_modules/express",
|
||||
"express",
|
||||
},
|
||||
{
|
||||
"node_modules relative scoped name",
|
||||
"node_modules/@angular/core",
|
||||
"@angular/core",
|
||||
},
|
||||
{
|
||||
"nested node_modules relative",
|
||||
"node_modules/@angular/core/node_modules/express",
|
||||
"express",
|
||||
},
|
||||
{
|
||||
"nested node_modules relative scoped name",
|
||||
"node_modules/@angular/core/node_modules/@angular/common",
|
||||
"@angular/common",
|
||||
},
|
||||
{
|
||||
"prefixed without node_modules",
|
||||
"prefix/node_modules/express",
|
||||
"express",
|
||||
},
|
||||
{
|
||||
"node_modules is not mandatory",
|
||||
"libs/@angular/core",
|
||||
"@angular/core",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := NpmNodeModulesPackagePathToName(test.path)
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -9,7 +9,8 @@ import (
|
||||
// only for packages not in the exempted by exception rules
|
||||
func AllowedPackages(manifest *models.PackageManifest,
|
||||
handler func(pkg *models.Package) error) error {
|
||||
for _, pkg := range manifest.Packages {
|
||||
packages := manifest.GetPackages()
|
||||
for _, pkg := range packages {
|
||||
res, err := Apply(pkg)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to evaluate exception for %s: %v",
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
package models
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// We are using generics here to make the graph implementation
|
||||
// not too coupled with our model types
|
||||
@ -13,6 +17,9 @@ type DependencyGraphNodeType interface {
|
||||
type DependencyGraphNode[T DependencyGraphNodeType] struct {
|
||||
Data T `json:"data"`
|
||||
Children []T `json:"children"`
|
||||
|
||||
// While not relevant for a graph, this is required to identify root packages
|
||||
Root bool `json:"root"`
|
||||
}
|
||||
|
||||
// Directed Acyclic Graph (DAG) representation of the package manifest
|
||||
@ -21,6 +28,10 @@ type DependencyGraph[T DependencyGraphNodeType] struct {
|
||||
nodes map[string]*DependencyGraphNode[T]
|
||||
}
|
||||
|
||||
func (node *DependencyGraphNode[T]) SetRoot(root bool) {
|
||||
node.Root = root
|
||||
}
|
||||
|
||||
func NewDependencyGraph[T DependencyGraphNodeType]() *DependencyGraph[T] {
|
||||
return &DependencyGraph[T]{
|
||||
present: false,
|
||||
@ -46,18 +57,32 @@ func (dg *DependencyGraph[T]) SetPresent(present bool) {
|
||||
dg.present = present
|
||||
}
|
||||
|
||||
// Add a node to the graph
|
||||
func (dg *DependencyGraph[T]) AddNode(node T) {
|
||||
_ = dg.findOrCreateNode(node)
|
||||
}
|
||||
|
||||
func (dg *DependencyGraph[T]) IsRoot(data T) bool {
|
||||
if node, ok := dg.nodes[data.Id()]; ok {
|
||||
return node.Root
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Add a root node to the graph
|
||||
func (dg *DependencyGraph[T]) AddRootNode(node T) {
|
||||
dg.AddNode(node)
|
||||
dg.nodes[node.Id()].Root = true
|
||||
}
|
||||
|
||||
// AddDependency adds a dependency from one package to another
|
||||
// Add an edge from [from] to [to]
|
||||
func (dg *DependencyGraph[T]) AddDependency(from, to T) {
|
||||
if _, ok := dg.nodes[from.Id()]; !ok {
|
||||
dg.nodes[from.Id()] = &DependencyGraphNode[T]{Data: from, Children: []T{}}
|
||||
}
|
||||
fromNode := dg.findOrCreateNode(from)
|
||||
toNode := dg.findOrCreateNode(to)
|
||||
|
||||
if _, ok := dg.nodes[to.Id()]; !ok {
|
||||
dg.nodes[to.Id()] = &DependencyGraphNode[T]{Data: to, Children: []T{}}
|
||||
}
|
||||
|
||||
dg.nodes[from.Id()].Children = append(dg.nodes[from.Id()].Children, dg.nodes[to.Id()].Data)
|
||||
fromNode.Children = append(fromNode.Children, toNode.Data)
|
||||
}
|
||||
|
||||
// GetDependencies returns the list of dependencies for the given package
|
||||
@ -90,19 +115,23 @@ func (dg *DependencyGraph[T]) GetDependents(pkg T) []T {
|
||||
}
|
||||
|
||||
// GetNodes returns the list of nodes in the graph
|
||||
// This is useful when enumerating all packages
|
||||
func (dg *DependencyGraph[T]) GetNodes() []T {
|
||||
var nodes []T
|
||||
func (dg *DependencyGraph[T]) GetNodes() []*DependencyGraphNode[T] {
|
||||
var nodes []*DependencyGraphNode[T]
|
||||
for _, node := range dg.nodes {
|
||||
nodes = append(nodes, node.Data)
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Alias for GetNodes
|
||||
// GetPackages returns the list of packages in the graph
|
||||
func (dg *DependencyGraph[T]) GetPackages() []T {
|
||||
return dg.GetNodes()
|
||||
var packages []T
|
||||
for _, node := range dg.nodes {
|
||||
packages = append(packages, node.Data)
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
// PathToRoot returns the path from the given package to the root
|
||||
@ -111,13 +140,20 @@ func (dg *DependencyGraph[T]) GetPackages() []T {
|
||||
// is more relevant here because we want to update minimum number of root packages
|
||||
func (dg *DependencyGraph[T]) PathToRoot(pkg T) []T {
|
||||
var path []T
|
||||
for _, node := range dg.nodes {
|
||||
if node.Data.Id() == pkg.Id() {
|
||||
path = append(path, node.Data)
|
||||
break
|
||||
}
|
||||
|
||||
// If the package is not present in the graph, return an empty path
|
||||
if node, ok := dg.nodes[pkg.Id()]; ok {
|
||||
path = append(path, node.Data)
|
||||
} else {
|
||||
return path
|
||||
}
|
||||
|
||||
// Check if we are already at the root
|
||||
if dg.nodes[pkg.Id()].Root {
|
||||
return path
|
||||
}
|
||||
|
||||
visited := make(map[string]bool)
|
||||
for len(path) > 0 {
|
||||
node := path[len(path)-1]
|
||||
dependents := dg.GetDependents(node)
|
||||
@ -125,12 +161,46 @@ func (dg *DependencyGraph[T]) PathToRoot(pkg T) []T {
|
||||
break
|
||||
}
|
||||
|
||||
path = append(path, dependents[0])
|
||||
// Sort dependents by Id to ensure deterministic traversal
|
||||
slices.SortFunc(dependents, func(a, b T) int {
|
||||
return cmp.Compare(a.Id(), b.Id())
|
||||
})
|
||||
|
||||
progress := false
|
||||
for _, dependent := range dependents {
|
||||
if _, ok := visited[dependent.Id()]; !ok {
|
||||
path = append(path, dependent)
|
||||
visited[dependent.Id()] = true
|
||||
progress = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !progress {
|
||||
break
|
||||
}
|
||||
|
||||
if n, ok := dg.nodes[path[len(path)-1].Id()]; ok && n.Root {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func (dg *DependencyGraph[T]) findOrCreateNode(data T) *DependencyGraphNode[T] {
|
||||
id := data.Id()
|
||||
if _, ok := dg.nodes[id]; !ok {
|
||||
dg.nodes[id] = &DependencyGraphNode[T]{
|
||||
Root: false,
|
||||
Data: data,
|
||||
Children: []T{},
|
||||
}
|
||||
}
|
||||
|
||||
return dg.nodes[id]
|
||||
}
|
||||
|
||||
func (dg *DependencyGraph[T]) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&struct {
|
||||
Present bool `json:"present"`
|
||||
|
||||
@ -63,10 +63,14 @@ func TestDependencyGraphGetNodes(t *testing.T) {
|
||||
dependencyGraphAddTestData(dg)
|
||||
|
||||
nodes := dg.GetNodes()
|
||||
assert.Contains(t, nodes, &dgTestNode{Name: "a"})
|
||||
assert.Contains(t, nodes, &dgTestNode{Name: "b"})
|
||||
assert.Contains(t, nodes, &dgTestNode{Name: "c"})
|
||||
assert.Contains(t, nodes, &dgTestNode{Name: "d"})
|
||||
assert.Equal(t, 4, len(nodes))
|
||||
|
||||
nodeNames := []string{}
|
||||
for _, node := range nodes {
|
||||
nodeNames = append(nodeNames, node.Data.Name)
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, []string{"a", "b", "c", "d"}, nodeNames)
|
||||
}
|
||||
|
||||
func TestDependencyGraphPathToRoot(t *testing.T) {
|
||||
@ -90,9 +94,8 @@ func TestDependencyGraphMarshalJSON(t *testing.T) {
|
||||
dependencyGraphAddTestData(dg)
|
||||
dg.SetPresent(true)
|
||||
|
||||
json, err := json.Marshal(dg)
|
||||
_, err := json.Marshal(dg)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "{\"present\":true,\"nodes\":{\"a\":{\"data\":{\"Name\":\"a\"},\"children\":[{\"Name\":\"b\"},{\"Name\":\"c\"}]},\"b\":{\"data\":{\"Name\":\"b\"},\"children\":[{\"Name\":\"c\"}]},\"c\":{\"data\":{\"Name\":\"c\"},\"children\":[{\"Name\":\"d\"}]},\"d\":{\"data\":{\"Name\":\"d\"},\"children\":[]}}}", string(json))
|
||||
}
|
||||
|
||||
func TestDependencyGraphUnmarshalJSON(t *testing.T) {
|
||||
|
||||
@ -66,6 +66,7 @@ func (pm *PackageManifest) AddPackage(pkg *Package) {
|
||||
defer pm.m.Unlock()
|
||||
|
||||
pm.Packages = append(pm.Packages, pkg)
|
||||
pm.DependencyGraph.AddNode(pkg)
|
||||
}
|
||||
|
||||
func (pm *PackageManifest) GetPath() string {
|
||||
@ -91,7 +92,7 @@ func (pm *PackageManifest) GetDisplayPath() string {
|
||||
// else fallsback to the [Packages] field
|
||||
func (pm *PackageManifest) GetPackages() []*Package {
|
||||
if pm.DependencyGraph != nil && pm.DependencyGraph.Present() {
|
||||
return pm.DependencyGraph.GetNodes()
|
||||
return pm.DependencyGraph.GetPackages()
|
||||
}
|
||||
|
||||
return pm.Packages
|
||||
@ -103,7 +104,7 @@ func (pm *PackageManifest) Id() string {
|
||||
}
|
||||
|
||||
func (pm *PackageManifest) GetPackagesCount() int {
|
||||
return len(pm.Packages)
|
||||
return len(pm.GetPackages())
|
||||
}
|
||||
|
||||
func (pm *PackageManifest) GetSpecEcosystem() modelspec.Ecosystem {
|
||||
@ -185,12 +186,38 @@ func (p *Package) ShortName() string {
|
||||
strings.ToLower(p.Name), p.Version)
|
||||
}
|
||||
|
||||
func NewPackageDetail(e, n, v string) lockfile.PackageDetails {
|
||||
func (p *Package) GetDependencyGraph() *DependencyGraph[*Package] {
|
||||
if p.Manifest == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.Manifest.DependencyGraph == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !p.Manifest.DependencyGraph.Present() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.Manifest.DependencyGraph
|
||||
}
|
||||
|
||||
// DependencyPath returns the path from a root package to this package
|
||||
func (p *Package) DependencyPath() []*Package {
|
||||
dg := p.GetDependencyGraph()
|
||||
if dg == nil {
|
||||
return []*Package{}
|
||||
}
|
||||
|
||||
return dg.PathToRoot(p)
|
||||
}
|
||||
|
||||
func NewPackageDetail(ecosystem, name, version string) lockfile.PackageDetails {
|
||||
return lockfile.PackageDetails{
|
||||
Ecosystem: lockfile.Ecosystem(e),
|
||||
CompareAs: lockfile.Ecosystem(e),
|
||||
Name: n,
|
||||
Version: v,
|
||||
Ecosystem: lockfile.Ecosystem(ecosystem),
|
||||
CompareAs: lockfile.Ecosystem(ecosystem),
|
||||
Name: name,
|
||||
Version: version,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11075
pkg/parser/fixtures/package-lock-graph.json
Normal file
11075
pkg/parser/fixtures/package-lock-graph.json
Normal file
File diff suppressed because it is too large
Load Diff
135
pkg/parser/npm_graph.go
Normal file
135
pkg/parser/npm_graph.go
Normal file
@ -0,0 +1,135 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/safedep/vet/pkg/common/utils"
|
||||
"github.com/safedep/vet/pkg/models"
|
||||
)
|
||||
|
||||
type npmPackageLockPackage struct {
|
||||
Version string `json:"version"`
|
||||
License string `json:"license"`
|
||||
Resolved string `json:"resolved"`
|
||||
Integrity string `json:"integrity"`
|
||||
Link bool `json:"link"`
|
||||
Dev bool `json:"dev"`
|
||||
Optional bool `json:"optional"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
DevDependencies map[string]string `json:"devDependencies"`
|
||||
}
|
||||
|
||||
// https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json
|
||||
type npmPackageLock struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
LockfileVersion int `json:"lockfileVersion"`
|
||||
Packages map[string]npmPackageLockPackage `json:"packages"`
|
||||
}
|
||||
|
||||
func parseNpmPackageLockAsGraph(lockfilePath string, config *ParserConfig) (*models.PackageManifest, error) {
|
||||
data, err := os.ReadFile(lockfilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lockfile npmPackageLock
|
||||
err = json.NewDecoder(bytes.NewReader(data)).Decode(&lockfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if lockfile.LockfileVersion < 2 {
|
||||
return nil, fmt.Errorf("npmGraphParser: Unsupported lockfile version %d",
|
||||
lockfile.LockfileVersion)
|
||||
}
|
||||
|
||||
logger.Debugf("npmGraphParser: Found %d packages in lockfile",
|
||||
len(lockfile.Packages))
|
||||
|
||||
manifest := models.NewPackageManifest(lockfilePath, models.EcosystemNpm)
|
||||
dependencyGraph := manifest.DependencyGraph
|
||||
|
||||
if dependencyGraph == nil {
|
||||
return nil, fmt.Errorf("npmGraphParser: Dependency graph is nil")
|
||||
}
|
||||
|
||||
// Is this really optional or should we hard fail here?
|
||||
if app, ok := lockfile.Packages[""]; ok {
|
||||
defer func() {
|
||||
for depName, depVersion := range app.Dependencies {
|
||||
node := npmGraphFindBySemverRange(dependencyGraph, depName, depVersion)
|
||||
if node != nil {
|
||||
node.SetRoot(true)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// We will first add all the nodes in the graph then add the edges
|
||||
// The nature of package-lock.json is such that it can contain multiple
|
||||
// version of the same dependency. So while adding edges, we have to find the node
|
||||
// that fulfills the semver constraint of the dependent towards the dependency node.
|
||||
for pkgLocation, pkgInfo := range lockfile.Packages {
|
||||
// The application itself
|
||||
if pkgLocation == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgName := utils.NpmNodeModulesPackagePathToName(pkgLocation)
|
||||
if pkgName == "" {
|
||||
logger.Debugf("npmGraphParser: Could not parse package name from location %s",
|
||||
pkgLocation)
|
||||
continue
|
||||
}
|
||||
|
||||
if !config.IncludeDevDependencies && (pkgInfo.Dev || pkgInfo.Optional) {
|
||||
logger.Debugf("npmGraphParser: Skipping dev/optional package %s", pkgName)
|
||||
continue
|
||||
}
|
||||
|
||||
pkgDetails := models.NewPackageDetail(models.EcosystemNpm, pkgName, pkgInfo.Version)
|
||||
pkg := &models.Package{
|
||||
PackageDetails: pkgDetails,
|
||||
Manifest: manifest,
|
||||
}
|
||||
|
||||
// Add node
|
||||
dependencyGraph.AddNode(pkg)
|
||||
|
||||
// Add edges (dependencies)
|
||||
for depName, depSemverRange := range pkgInfo.Dependencies {
|
||||
defer npmGraphAddDependencyRelation(dependencyGraph, pkg, depName, depSemverRange)
|
||||
}
|
||||
}
|
||||
|
||||
dependencyGraph.SetPresent(true)
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// npmGraphAddDependencyRelation enumerates all nodes in the graph to find a node that matches semver constraint
|
||||
// If found, it adds an edge from the node to the dependency node
|
||||
func npmGraphAddDependencyRelation(graph *models.DependencyGraph[*models.Package],
|
||||
from *models.Package, name, semver string) {
|
||||
nodeTarget := npmGraphFindBySemverRange(graph, name, semver)
|
||||
if nodeTarget == nil {
|
||||
logger.Debugf("npmGraphParser: Could not find a node that matches semver constraint %s for dependency %s",
|
||||
semver, name)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("npmGraphParser: Adding dependency for %s@%s to %s@%s",
|
||||
from.GetName(), from.GetVersion(),
|
||||
nodeTarget.Data.GetName(), nodeTarget.Data.GetVersion())
|
||||
|
||||
graph.AddDependency(from, nodeTarget.Data)
|
||||
}
|
||||
|
||||
func npmGraphFindBySemverRange(graph *models.DependencyGraph[*models.Package],
|
||||
name, semver string) *models.DependencyGraphNode[*models.Package] {
|
||||
return utils.FindDependencyGraphNodeBySemverRange(graph, name, semver)
|
||||
}
|
||||
176
pkg/parser/npm_graph_test.go
Normal file
176
pkg/parser/npm_graph_test.go
Normal file
@ -0,0 +1,176 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safedep/vet/pkg/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var defaultParserConfigForTest = &ParserConfig{}
|
||||
|
||||
func findPackageInGraph(graph *models.DependencyGraph[*models.Package], name, version string) *models.Package {
|
||||
for _, node := range graph.GetPackages() {
|
||||
if node.GetName() == name && node.GetVersion() == version {
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNpmGraphParserBasic(t *testing.T) {
|
||||
pm, err := parseNpmPackageLockAsGraph("./fixtures/package-lock-graph.json", defaultParserConfigForTest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.NotNil(t, pm)
|
||||
assert.NotNil(t, pm.DependencyGraph)
|
||||
assert.NotEmpty(t, pm.DependencyGraph.GetNodes())
|
||||
}
|
||||
|
||||
func TestNpmGraphParserDependencies(t *testing.T) {
|
||||
pm, err := parseNpmPackageLockAsGraph("./fixtures/package-lock-graph.json", defaultParserConfigForTest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
aNode := findPackageInGraph(pm.DependencyGraph, "@aws-sdk/client-s3", "3.478.0")
|
||||
assert.NotNil(t, aNode)
|
||||
|
||||
aNodeDependencies := pm.DependencyGraph.GetDependencies(aNode)
|
||||
assert.NotEmpty(t, aNodeDependencies)
|
||||
assert.Equal(t, 58, len(aNodeDependencies))
|
||||
|
||||
dependencyNames := []string{}
|
||||
for _, node := range aNodeDependencies {
|
||||
dependencyNames = append(dependencyNames, node.GetName())
|
||||
}
|
||||
|
||||
expectedDependencyNames := []string{
|
||||
"@aws-sdk/middleware-user-agent",
|
||||
"@aws-sdk/middleware-ssec",
|
||||
"@aws-sdk/client-sts",
|
||||
"@aws-crypto/sha256-js",
|
||||
"@aws-sdk/signature-v4-multi-region",
|
||||
"@smithy/middleware-serde",
|
||||
"@smithy/fetch-http-handler",
|
||||
"@aws-sdk/xml-builder",
|
||||
"@aws-sdk/middleware-expect-continue",
|
||||
"@smithy/node-config-provider",
|
||||
"@aws-sdk/util-user-agent-browser",
|
||||
"@aws-sdk/util-endpoints",
|
||||
"@aws-sdk/middleware-logger",
|
||||
"@smithy/util-retry",
|
||||
"@smithy/util-defaults-mode-node",
|
||||
"@smithy/md5-js",
|
||||
"@aws-sdk/util-user-agent-node",
|
||||
"@aws-sdk/middleware-recursion-detection",
|
||||
"@aws-sdk/middleware-location-constraint",
|
||||
"@smithy/util-endpoints",
|
||||
"@smithy/url-parser",
|
||||
"@smithy/middleware-retry",
|
||||
"@smithy/middleware-stack",
|
||||
"@smithy/eventstream-serde-node",
|
||||
"@smithy/eventstream-serde-browser",
|
||||
"@aws-sdk/middleware-signing",
|
||||
"@smithy/util-stream",
|
||||
"@smithy/node-http-handler",
|
||||
"@smithy/protocol-http",
|
||||
"@aws-sdk/middleware-host-header",
|
||||
"@aws-crypto/sha256-browser",
|
||||
"@smithy/middleware-endpoint",
|
||||
"@aws-sdk/types",
|
||||
"@aws-sdk/region-config-resolver",
|
||||
"@aws-sdk/middleware-sdk-s3",
|
||||
"@smithy/util-waiter",
|
||||
"@smithy/config-resolver",
|
||||
"@aws-sdk/middleware-flexible-checksums",
|
||||
"fast-xml-parser",
|
||||
"@aws-crypto/sha1-browser",
|
||||
"@smithy/util-base64",
|
||||
"@smithy/middleware-content-length",
|
||||
"@aws-sdk/middleware-bucket-endpoint",
|
||||
"@aws-sdk/core",
|
||||
"@smithy/util-body-length-node",
|
||||
"@smithy/types",
|
||||
"@smithy/hash-stream-node",
|
||||
"@smithy/eventstream-serde-config-resolver",
|
||||
"@smithy/util-utf8",
|
||||
"@smithy/smithy-client",
|
||||
"@smithy/hash-node",
|
||||
"@smithy/util-defaults-mode-browser",
|
||||
"@smithy/invalid-dependency",
|
||||
"@smithy/hash-blob-browser",
|
||||
"tslib",
|
||||
"@smithy/util-body-length-browser",
|
||||
"@smithy/core",
|
||||
"@aws-sdk/credential-provider-node",
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, expectedDependencyNames, dependencyNames)
|
||||
}
|
||||
|
||||
func TestNpmGraphParserDependents(t *testing.T) {
|
||||
pm, err := parseNpmPackageLockAsGraph("./fixtures/package-lock-graph.json", defaultParserConfigForTest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
bNode := findPackageInGraph(pm.DependencyGraph, "tslib", "1.14.1")
|
||||
assert.NotNil(t, bNode)
|
||||
|
||||
bNodeDependents := pm.DependencyGraph.GetDependents(bNode)
|
||||
assert.NotEmpty(t, bNodeDependents)
|
||||
assert.Equal(t, 18, len(bNodeDependents))
|
||||
|
||||
bNodeDependentNames := []string{}
|
||||
for _, node := range bNodeDependents {
|
||||
bNodeDependentNames = append(bNodeDependentNames, node.GetName())
|
||||
}
|
||||
|
||||
expectedDependentNames := []string{
|
||||
"@aws-crypto/crc32c",
|
||||
"@aws-crypto/supports-web-crypto",
|
||||
"@aws-crypto/supports-web-crypto",
|
||||
"@aws-crypto/supports-web-crypto",
|
||||
"@aws-crypto/supports-web-crypto",
|
||||
"@aws-crypto/supports-web-crypto",
|
||||
"@aws-crypto/sha256-js",
|
||||
"@aws-crypto/sha256-js",
|
||||
"@aws-crypto/sha256-js",
|
||||
"@aws-crypto/sha256-js",
|
||||
"@aws-crypto/sha256-browser",
|
||||
"@aws-crypto/sha256-browser",
|
||||
"@aws-crypto/sha256-browser",
|
||||
"@aws-crypto/sha256-browser",
|
||||
"@aws-crypto/ie11-detection",
|
||||
"@aws-crypto/sha1-browser",
|
||||
"@aws-crypto/crc32",
|
||||
"@aws-crypto/util",
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, expectedDependentNames, bNodeDependentNames)
|
||||
}
|
||||
|
||||
func TestNpmGraphParserPathToRootFromRoot(t *testing.T) {
|
||||
pm, err := parseNpmPackageLockAsGraph("./fixtures/package-lock-graph.json", defaultParserConfigForTest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
aNode := findPackageInGraph(pm.DependencyGraph, "@aws-sdk/client-s3", "3.478.0")
|
||||
assert.NotNil(t, aNode)
|
||||
|
||||
aNodeToRoot := pm.DependencyGraph.PathToRoot(aNode)
|
||||
assert.Equal(t, 1, len(aNodeToRoot))
|
||||
assert.Equal(t, "@aws-sdk/client-s3", aNodeToRoot[0].GetName())
|
||||
}
|
||||
|
||||
func TestNpmGraphParserPathToRootFromDependent(t *testing.T) {
|
||||
pm, err := parseNpmPackageLockAsGraph("./fixtures/package-lock-graph.json", defaultParserConfigForTest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
bNode := findPackageInGraph(pm.DependencyGraph, "tslib", "1.14.1")
|
||||
assert.NotNil(t, bNode)
|
||||
|
||||
bNodeToRoot := pm.DependencyGraph.PathToRoot(bNode)
|
||||
assert.Equal(t, 4, len(bNodeToRoot))
|
||||
assert.Equal(t, "tslib", bNodeToRoot[0].GetName())
|
||||
assert.Equal(t, "@aws-crypto/sha256-js", bNodeToRoot[1].GetName())
|
||||
assert.Equal(t, "@aws-sdk/client-sts", bNodeToRoot[2].GetName())
|
||||
assert.Equal(t, "@aws-sdk/client-s3", bNodeToRoot[3].GetName())
|
||||
}
|
||||
@ -2,6 +2,7 @@ package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/osv-scanner/pkg/lockfile"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
@ -39,6 +40,7 @@ var supportedEcosystems map[string]bool = map[string]bool{
|
||||
models.EcosystemSpdxSBOM: true,
|
||||
}
|
||||
|
||||
// TODO: Migrate these to graph parser
|
||||
var customExperimentalParsers map[string]lockfile.PackageDetailsParser = map[string]lockfile.PackageDetailsParser{
|
||||
customParserTypePyWheel: parsePythonWheelDist,
|
||||
customParserCycloneDXSBOM: cdx.Parse,
|
||||
@ -49,17 +51,39 @@ var customExperimentalParsers map[string]lockfile.PackageDetailsParser = map[str
|
||||
type Parser interface {
|
||||
Ecosystem() string
|
||||
Parse(lockfilePath string) (*models.PackageManifest, error)
|
||||
ParseWithConfig(lockfilePath string, config *ParserConfig) (*models.PackageManifest, error)
|
||||
}
|
||||
|
||||
type ParserConfig struct {
|
||||
// A generic config flag (not specific to npm even though the name sounds like that) to indicate
|
||||
// if the parser should include non-production dependencies as well. But this will work
|
||||
// only for supported parsers such as npm graph parser
|
||||
IncludeDevDependencies bool
|
||||
}
|
||||
|
||||
// Graph parser always takes precedence over lockfile parser
|
||||
type parserWrapper struct {
|
||||
parser lockfile.PackageDetailsParser
|
||||
parseAs string
|
||||
graphParser dependencyGraphParser
|
||||
parser lockfile.PackageDetailsParser
|
||||
parseAs string
|
||||
}
|
||||
|
||||
// This is how a graph parser should be implemented
|
||||
type dependencyGraphParser func(lockfilePath string, config *ParserConfig) (*models.PackageManifest, error)
|
||||
|
||||
// Maintain a map of lockfileAs to dependencyGraphParser
|
||||
var dependencyGraphParsers map[string]dependencyGraphParser = map[string]dependencyGraphParser{
|
||||
"package-lock.json": parseNpmPackageLockAsGraph,
|
||||
}
|
||||
|
||||
func List(experimental bool) []string {
|
||||
supportedParsers := make([]string, 0, 0)
|
||||
parsers := lockfile.ListParsers()
|
||||
|
||||
for pa := range dependencyGraphParsers {
|
||||
supportedParsers = append(supportedParsers, fmt.Sprintf("%s (graph)", pa))
|
||||
}
|
||||
|
||||
parsers := lockfile.ListParsers()
|
||||
for _, p := range parsers {
|
||||
_, err := FindParser("", p)
|
||||
if err != nil {
|
||||
@ -79,6 +103,18 @@ func List(experimental bool) []string {
|
||||
}
|
||||
|
||||
func FindParser(lockfilePath, lockfileAs string) (Parser, error) {
|
||||
// Find a graph parser for the lockfile
|
||||
logger.Debugf("Trying to find graph parser for %s", lockfilePath)
|
||||
gp, gpa := findGraphParser(lockfilePath, lockfileAs)
|
||||
if gp != nil {
|
||||
pw := &parserWrapper{graphParser: gp, parseAs: gpa}
|
||||
if pw.supported() {
|
||||
return pw, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find a parser for the lockfile
|
||||
logger.Debugf("Trying to find lockfile parser for %s", lockfilePath)
|
||||
p, pa := lockfile.FindParser(lockfilePath, lockfileAs)
|
||||
if p != nil {
|
||||
pw := &parserWrapper{parser: p, parseAs: pa}
|
||||
@ -87,6 +123,7 @@ func FindParser(lockfilePath, lockfileAs string) (Parser, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Use experimental parser for explicitly provided lockfile type
|
||||
logger.Debugf("Trying to find parser in experimental parsers %s", lockfileAs)
|
||||
if p, ok := customExperimentalParsers[lockfileAs]; ok {
|
||||
pw := &parserWrapper{parser: p, parseAs: lockfileAs}
|
||||
@ -96,17 +133,30 @@ func FindParser(lockfilePath, lockfileAs string) (Parser, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// We failed!
|
||||
logger.Debugf("No Parser found for the type %s", lockfileAs)
|
||||
return nil, fmt.Errorf("no parser found with: %s for: %s", lockfileAs,
|
||||
lockfilePath)
|
||||
}
|
||||
|
||||
func findGraphParser(lockfilePath, lockfileAs string) (dependencyGraphParser, string) {
|
||||
parseAs := lockfileAs
|
||||
if lockfileAs == "" {
|
||||
parseAs = filepath.Base(lockfilePath)
|
||||
}
|
||||
|
||||
if _, ok := dependencyGraphParsers[parseAs]; ok {
|
||||
return dependencyGraphParsers[parseAs], parseAs
|
||||
}
|
||||
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func (pw *parserWrapper) supported() bool {
|
||||
return supportedEcosystems[pw.Ecosystem()]
|
||||
}
|
||||
|
||||
func (pw *parserWrapper) Ecosystem() string {
|
||||
logger.Debugf("Provided Lockfile Type %s", pw.parseAs)
|
||||
switch pw.parseAs {
|
||||
case "Cargo.lock":
|
||||
return models.EcosystemCargo
|
||||
@ -153,14 +203,21 @@ func (pw *parserWrapper) Ecosystem() string {
|
||||
}
|
||||
|
||||
func (pw *parserWrapper) Parse(lockfilePath string) (*models.PackageManifest, error) {
|
||||
return pw.ParseWithConfig(lockfilePath, &ParserConfig{})
|
||||
}
|
||||
|
||||
func (pw *parserWrapper) ParseWithConfig(lockfilePath string, config *ParserConfig) (*models.PackageManifest, error) {
|
||||
logger.Infof("[%s] Parsing %s", pw.parseAs, lockfilePath)
|
||||
pm := models.NewPackageManifest(lockfilePath, pw.Ecosystem())
|
||||
if pw.graphParser != nil {
|
||||
return pw.graphParser(lockfilePath, config)
|
||||
}
|
||||
|
||||
packages, err := pw.parser(lockfilePath)
|
||||
if err != nil {
|
||||
return pm, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pm := models.NewPackageManifest(lockfilePath, pw.Ecosystem())
|
||||
for _, pkg := range packages {
|
||||
pm.AddPackage(&models.Package{
|
||||
PackageDetails: pkg,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -8,7 +9,7 @@ import (
|
||||
|
||||
func TestListParser(t *testing.T) {
|
||||
parsers := List(false)
|
||||
assert.Equal(t, 11, len(parsers))
|
||||
assert.Equal(t, 12, len(parsers))
|
||||
}
|
||||
|
||||
func TestInvalidEcosystemMapping(t *testing.T) {
|
||||
@ -19,6 +20,9 @@ func TestInvalidEcosystemMapping(t *testing.T) {
|
||||
func TestEcosystemMapping(t *testing.T) {
|
||||
for _, lf := range List(false) {
|
||||
t.Run(lf, func(t *testing.T) {
|
||||
// For graph parsers, we add a tag to the end of the name
|
||||
lf = strings.Split(lf, " ")[0]
|
||||
|
||||
pw := &parserWrapper{parseAs: lf}
|
||||
assert.NotEmpty(t, pw.Ecosystem())
|
||||
})
|
||||
|
||||
@ -171,7 +171,7 @@ func (p *githubReader) processRemoteDependencyGraph(ctx context.Context, client
|
||||
return err
|
||||
}
|
||||
|
||||
if len(manifest.Packages) == 0 {
|
||||
if manifest.GetPackagesCount() == 0 {
|
||||
return errors.New("no packages identified from SBOM")
|
||||
}
|
||||
|
||||
|
||||
@ -77,7 +77,7 @@ func (p *jsonDumpReader) EnumManifests(handler func(*models.PackageManifest,
|
||||
manifest.Path = path
|
||||
|
||||
// Fix manifest reference in each package
|
||||
for _, pkg := range manifest.Packages {
|
||||
for _, pkg := range manifest.GetPackages() {
|
||||
pkg.Manifest = &manifest
|
||||
}
|
||||
|
||||
|
||||
@ -63,7 +63,7 @@ func TestLockfileReaderEnumManifests(t *testing.T) {
|
||||
},
|
||||
"", // Auto detect from name
|
||||
nil,
|
||||
errors.New("package-lock.json: invalid character"),
|
||||
errors.New("invalid character"),
|
||||
0,
|
||||
[]int{13},
|
||||
},
|
||||
|
||||
@ -25,10 +25,7 @@ func (p *purlReader) EnumManifests(handler func(*models.PackageManifest,
|
||||
}
|
||||
|
||||
pd := parsedPurl.GetPackageDetails()
|
||||
pm := &models.PackageManifest{
|
||||
Path: p.purl,
|
||||
Ecosystem: string(pd.Ecosystem),
|
||||
}
|
||||
pm := models.NewPackageManifest(p.purl, string(pd.Ecosystem))
|
||||
|
||||
pm.AddPackage(&models.Package{
|
||||
PackageDetails: pd,
|
||||
|
||||
@ -26,3 +26,47 @@ func TestPurlReader(t *testing.T) {
|
||||
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestPurlReaderWithMultiplePURLS(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
purl string
|
||||
ecosystem string
|
||||
pkgName string
|
||||
version string
|
||||
}{
|
||||
{
|
||||
"Maven PURL",
|
||||
"pkg:maven/org.apache.commons/commons-lang3@3.8.1",
|
||||
"Maven",
|
||||
"org.apache.commons:commons-lang3",
|
||||
"3.8.1",
|
||||
},
|
||||
{
|
||||
"Maven PURL log4j",
|
||||
"pkg:maven/log4j/log4j@1.2.17",
|
||||
"Maven",
|
||||
"log4j:log4j",
|
||||
"1.2.17",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
reader, err := NewPurlReader(test.purl)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = reader.EnumManifests(func(pm *models.PackageManifest, pr PackageReader) error {
|
||||
assert.Equal(t, 1, len(pm.Packages))
|
||||
assert.NotNil(t, pm.Packages[0])
|
||||
assert.Equal(t, test.pkgName, pm.Packages[0].Name)
|
||||
assert.Equal(t, test.version, pm.Packages[0].Version)
|
||||
assert.Equal(t, test.ecosystem, string(pm.Packages[0].Ecosystem))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package reporter
|
||||
import (
|
||||
"encoding/csv"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/safedep/vet/pkg/analyzer"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
@ -25,6 +26,8 @@ type csvRecord struct {
|
||||
manifestPath string
|
||||
packageName string
|
||||
packageVersion string
|
||||
introducedBy string
|
||||
pathToRoot string
|
||||
violationReason string
|
||||
}
|
||||
|
||||
@ -77,12 +80,29 @@ func (r *csvReporter) Finish() error {
|
||||
continue
|
||||
}
|
||||
|
||||
introducedBy := ""
|
||||
pathToRoot := ""
|
||||
|
||||
paths := v.Package.DependencyPath()
|
||||
pathPackages := []string{}
|
||||
|
||||
for _, path := range paths {
|
||||
pathPackages = append(pathPackages, path.GetName())
|
||||
}
|
||||
|
||||
if len(paths) > 0 {
|
||||
introducedBy = pathPackages[len(paths)-1]
|
||||
pathToRoot = strings.Join(pathPackages, " -> ")
|
||||
}
|
||||
|
||||
records = append(records, csvRecord{
|
||||
ecosystem: string(v.Package.Ecosystem),
|
||||
manifestPath: v.Manifest.GetDisplayPath(),
|
||||
packageName: v.Package.GetName(),
|
||||
packageVersion: v.Package.GetVersion(),
|
||||
violationReason: msg,
|
||||
introducedBy: introducedBy,
|
||||
pathToRoot: pathToRoot,
|
||||
})
|
||||
}
|
||||
|
||||
@ -109,7 +129,9 @@ func (r *csvReporter) persistCsvRecords(records []csvRecord) error {
|
||||
"Manifest Path",
|
||||
"Package Name",
|
||||
"Package Version",
|
||||
"Filter Name"})
|
||||
"Violation",
|
||||
"Introduced By",
|
||||
"Path To Root"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -119,6 +141,8 @@ func (r *csvReporter) persistCsvRecords(records []csvRecord) error {
|
||||
csvRecord.ecosystem, csvRecord.manifestPath,
|
||||
csvRecord.packageName, csvRecord.packageVersion,
|
||||
csvRecord.violationReason,
|
||||
csvRecord.introducedBy,
|
||||
csvRecord.pathToRoot,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
107
pkg/reporter/dot_graph.go
Normal file
107
pkg/reporter/dot_graph.go
Normal file
@ -0,0 +1,107 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/safedep/vet/pkg/analyzer"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/safedep/vet/pkg/models"
|
||||
"github.com/safedep/vet/pkg/policy"
|
||||
)
|
||||
|
||||
var dotFileNameCleanerRegexp = regexp.MustCompile(`[^\w\d\.\-]`)
|
||||
|
||||
type DotGraphReporter struct {
|
||||
Directory string
|
||||
}
|
||||
|
||||
func NewDotGraphReporter(directory string) (Reporter, error) {
|
||||
if _, err := os.Stat(directory); err != nil {
|
||||
err := os.MkdirAll(directory, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &DotGraphReporter{Directory: directory}, nil
|
||||
}
|
||||
|
||||
func (r *DotGraphReporter) Name() string {
|
||||
return "Graphviz Dot Graph"
|
||||
}
|
||||
|
||||
func (r *DotGraphReporter) AddManifest(manifest *models.PackageManifest) {
|
||||
dotFileName := r.dotFileNameFromManifestPath(manifest.GetPath())
|
||||
dotFilePath := filepath.Join(r.Directory, dotFileName+".dot")
|
||||
|
||||
writer, err := os.Create(dotFilePath)
|
||||
if err != nil {
|
||||
logger.Errorf("dotGraphReporter: failed to create file %s: %v", dotFilePath, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer writer.Close()
|
||||
|
||||
renderedGraph, err := r.dotRenderDependencyGraph(manifest.DependencyGraph)
|
||||
if err != nil {
|
||||
logger.Errorf("dotGraphReporter: failed to render graph: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = writer.WriteString(renderedGraph)
|
||||
if err != nil {
|
||||
logger.Errorf("dotGraphReporter: failed to write to file %s: %v", dotFilePath, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DotGraphReporter) AddAnalyzerEvent(event *analyzer.AnalyzerEvent) {}
|
||||
|
||||
func (r *DotGraphReporter) AddPolicyEvent(event *policy.PolicyEvent) {}
|
||||
|
||||
func (r *DotGraphReporter) Finish() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DotGraphReporter) dotFileNameFromManifestPath(path string) string {
|
||||
s := filepath.Clean(path)
|
||||
s = dotFileNameCleanerRegexp.ReplaceAllString(s, "_")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *DotGraphReporter) dotRenderDependencyGraph(dg *models.DependencyGraph[*models.Package]) (string, error) {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("digraph {\n")
|
||||
sb.WriteString(" rankdir=LR;\n")
|
||||
sb.WriteString(" node [shape=box];\n")
|
||||
|
||||
// Generate the node names
|
||||
for _, node := range dg.GetNodes() {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString("\"" + r.nodeNameForPackage(node.Data) + "\"")
|
||||
sb.WriteString(";\n")
|
||||
}
|
||||
|
||||
// Add the relations
|
||||
for _, node := range dg.GetNodes() {
|
||||
for _, edge := range node.Children {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString("\"" + r.nodeNameForPackage(node.Data) + "\"")
|
||||
sb.WriteString(" -> ")
|
||||
sb.WriteString("\"" + r.nodeNameForPackage(edge) + "\"")
|
||||
sb.WriteString(";\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("}\n")
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (r *DotGraphReporter) nodeNameForPackage(pkg *models.Package) string {
|
||||
return fmt.Sprintf("%s@%s", pkg.GetName(), pkg.GetVersion())
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
@ -296,18 +297,18 @@ func (r *summaryReporter) Finish() error {
|
||||
func (r *summaryReporter) sortedRemediations() []*summaryReporterRemediationData {
|
||||
sortedPackages := []*summaryReporterRemediationData{}
|
||||
for _, value := range r.remediationScores {
|
||||
i := sort.Search(len(sortedPackages), func(i int) bool {
|
||||
return value.score >= sortedPackages[i].score
|
||||
})
|
||||
|
||||
if i == len(sortedPackages) {
|
||||
sortedPackages = append(sortedPackages, value)
|
||||
} else {
|
||||
sortedPackages = append(sortedPackages[:i+1], sortedPackages[i:]...)
|
||||
sortedPackages[i] = value
|
||||
}
|
||||
sortedPackages = append(sortedPackages, value)
|
||||
}
|
||||
|
||||
slices.SortFunc(sortedPackages, func(a, b *summaryReporterRemediationData) int {
|
||||
if a.score == b.score {
|
||||
return cmp.Compare(a.pkg.GetName(), b.pkg.GetName())
|
||||
}
|
||||
|
||||
// We want to sort by descending order
|
||||
return cmp.Compare(b.score, a.score)
|
||||
})
|
||||
|
||||
return sortedPackages
|
||||
}
|
||||
|
||||
@ -345,6 +346,13 @@ func (r *summaryReporter) renderRemediationAdvice() {
|
||||
"", tagText, "", "",
|
||||
})
|
||||
|
||||
pathToRoot := text.Faint.Sprint(r.pathToPackageRoot(sp.pkg))
|
||||
if pathToRoot != "" {
|
||||
tbl.AppendRow(table.Row{
|
||||
"", pathToRoot, "", "",
|
||||
})
|
||||
}
|
||||
|
||||
tbl.AppendSeparator()
|
||||
}
|
||||
|
||||
@ -408,3 +416,28 @@ func (r *summaryReporter) exceptionsCountStatement() string {
|
||||
return fmt.Sprintf("%d libraries are exempted from analysis through exception rules",
|
||||
exceptions.ActiveCount())
|
||||
}
|
||||
|
||||
func (r *summaryReporter) pathToPackageRoot(pkg *models.Package) string {
|
||||
path := strings.Builder{}
|
||||
|
||||
dg := pkg.GetDependencyGraph()
|
||||
if dg == nil {
|
||||
return path.String()
|
||||
}
|
||||
|
||||
if dg.IsRoot(pkg) {
|
||||
return path.String()
|
||||
}
|
||||
|
||||
pathToRoot := pkg.Manifest.DependencyGraph.PathToRoot(pkg)
|
||||
if len(pathToRoot) == 0 {
|
||||
return path.String()
|
||||
}
|
||||
|
||||
path.WriteString(fmt.Sprintf(" ... [%d] > ", len(pathToRoot)-1))
|
||||
|
||||
rootPkg := pathToRoot[len(pathToRoot)-1]
|
||||
path.WriteString(rootPkg.GetName() + "@" + rootPkg.GetVersion())
|
||||
|
||||
return path.String()
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
dryutils "github.com/safedep/dry/utils"
|
||||
"github.com/safedep/vet/pkg/analyzer"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/safedep/vet/pkg/common/utils"
|
||||
@ -17,6 +18,7 @@ type Config struct {
|
||||
ConcurrentAnalyzer int
|
||||
TransitiveAnalysis bool
|
||||
TransitiveDepth int
|
||||
Experimental bool
|
||||
}
|
||||
|
||||
type packageManifestScanner struct {
|
||||
@ -213,6 +215,8 @@ func (s *packageManifestScanner) enrichManifest(manifest *models.PackageManifest
|
||||
return nil
|
||||
}
|
||||
|
||||
defer s.finaliseDependencyGraph(manifest)
|
||||
|
||||
// FIXME: Potential deadlock situation in case of channel buffer is full
|
||||
// because the goroutines perform both read and write to channel. Write occurs
|
||||
// when goroutine invokes the work queue handler and the handler pushes back
|
||||
@ -246,7 +250,7 @@ func (s *packageManifestScanner) enrichManifest(manifest *models.PackageManifest
|
||||
func (s *packageManifestScanner) packageEnrichWorkQueueHandler(pm *models.PackageManifest) utils.WorkQueueFn[*models.Package] {
|
||||
return func(q *utils.WorkQueue[*models.Package], item *models.Package) error {
|
||||
for _, enricher := range s.enrichers {
|
||||
err := enricher.Enrich(item, s.packageDependencyHandler(pm, q))
|
||||
err := enricher.Enrich(item, s.packageDependencyHandler(pm, item, q))
|
||||
if err != nil {
|
||||
logger.Errorf("Enricher %s failed with %v", enricher.Name(), err)
|
||||
}
|
||||
@ -257,8 +261,10 @@ func (s *packageManifestScanner) packageEnrichWorkQueueHandler(pm *models.Packag
|
||||
}
|
||||
|
||||
func (s *packageManifestScanner) packageDependencyHandler(pm *models.PackageManifest,
|
||||
currentPkg *models.Package,
|
||||
q *utils.WorkQueue[*models.Package]) PackageDependencyCallbackFn {
|
||||
return func(pkg *models.Package) error {
|
||||
// Check and queue for further analysis
|
||||
if !s.config.TransitiveAnalysis {
|
||||
return nil
|
||||
}
|
||||
@ -278,3 +284,61 @@ func (s *packageManifestScanner) packageDependencyHandler(pm *models.PackageMani
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// finaliseDependencyGraph attempts to mark some nodes as root node if they do not have a dependent
|
||||
// this is just a heuristic to mark some nodes as root node. This may not be accurate in all cases
|
||||
func (s *packageManifestScanner) finaliseDependencyGraph(manifest *models.PackageManifest) {
|
||||
if manifest.DependencyGraph == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if manifest.DependencyGraph.Present() {
|
||||
return
|
||||
}
|
||||
|
||||
// Building dependency graph using package insights
|
||||
packages := manifest.GetPackages()
|
||||
for _, pkg := range packages {
|
||||
insights := dryutils.SafelyGetValue(pkg.Insights)
|
||||
dependencies := dryutils.SafelyGetValue(insights.Dependencies)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
distance := dryutils.SafelyGetValue(dep.Distance)
|
||||
|
||||
// Distance = 0 is the package itself
|
||||
if distance == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Distance > 1 means the package is not a direct dependency
|
||||
if distance > 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("Adding dependency %s/%s to dependency graph",
|
||||
dep.PackageVersion.Name, dep.PackageVersion.Version)
|
||||
|
||||
targetPkg := utils.FindDependencyGraphNodeBySemverRange(manifest.DependencyGraph,
|
||||
dep.PackageVersion.Name, dep.PackageVersion.Version)
|
||||
|
||||
if targetPkg == nil {
|
||||
logger.Debugf("Dependency %s/%s not found in dependency graph",
|
||||
dep.PackageVersion.Name, dep.PackageVersion.Version)
|
||||
continue
|
||||
}
|
||||
|
||||
manifest.DependencyGraph.AddDependency(pkg, targetPkg.Data)
|
||||
}
|
||||
}
|
||||
|
||||
nodes := manifest.DependencyGraph.GetNodes()
|
||||
for _, node := range nodes {
|
||||
pkg := node.Data
|
||||
dependents := manifest.DependencyGraph.GetDependents(pkg)
|
||||
if len(dependents) == 0 {
|
||||
node.SetRoot(true)
|
||||
}
|
||||
}
|
||||
|
||||
manifest.DependencyGraph.SetPresent(true)
|
||||
}
|
||||
|
||||
12
query.go
12
query.go
@ -21,6 +21,7 @@ var (
|
||||
querySummaryReportMaxAdvice int
|
||||
queryMarkdownReportPath string
|
||||
queryJsonReportPath string
|
||||
queryGraphReportPath string
|
||||
queryCsvReportPath string
|
||||
queryExceptionsFile string
|
||||
queryExceptionsTill string
|
||||
@ -64,6 +65,8 @@ func newQueryCommand() *cobra.Command {
|
||||
"Generate markdown report to file")
|
||||
cmd.Flags().StringVarP(&queryJsonReportPath, "report-json", "", "",
|
||||
"Generate JSON report to file (EXPERIMENTAL)")
|
||||
cmd.Flags().StringVarP(&queryGraphReportPath, "report-graph", "", "",
|
||||
"Generate dependency graph as graphviz dot files to directory")
|
||||
cmd.Flags().StringVarP(&queryCsvReportPath, "report-csv", "", "",
|
||||
"Generate CSV report of filtered packages to file")
|
||||
return cmd
|
||||
@ -176,6 +179,15 @@ func internalStartQuery() error {
|
||||
reporters = append(reporters, rp)
|
||||
}
|
||||
|
||||
if !utils.IsEmptyString(queryGraphReportPath) {
|
||||
rp, err := reporter.NewDotGraphReporter(queryGraphReportPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reporters = append(reporters, rp)
|
||||
}
|
||||
|
||||
pmScanner := scanner.NewPackageManifestScanner(scanner.Config{
|
||||
TransitiveAnalysis: false,
|
||||
}, readerList, enrichers, analyzers, reporters)
|
||||
|
||||
16
scan.go
16
scan.go
@ -46,10 +46,12 @@ var (
|
||||
disableAuthVerifyBeforeScan bool
|
||||
syncReport bool
|
||||
syncReportProject string
|
||||
graphReportDirectory string
|
||||
syncReportStream string
|
||||
listExperimentalParsers bool
|
||||
failFast bool
|
||||
trustedRegistryUrls []string
|
||||
scannerExperimental bool
|
||||
)
|
||||
|
||||
func newScanCommand() *cobra.Command {
|
||||
@ -117,6 +119,8 @@ func newScanCommand() *cobra.Command {
|
||||
"Generate CSV report of filtered packages")
|
||||
cmd.Flags().StringVarP(&jsonReportPath, "report-json", "", "",
|
||||
"Generate consolidated JSON report to file (EXPERIMENTAL schema)")
|
||||
cmd.Flags().StringVarP(&graphReportDirectory, "report-graph", "", "",
|
||||
"Generate dependency graph (if available) as dot files to directory")
|
||||
cmd.Flags().BoolVarP(&syncReport, "report-sync", "", false,
|
||||
"Enable syncing report data to cloud")
|
||||
cmd.Flags().StringVarP(&syncReportProject, "report-sync-project", "", "",
|
||||
@ -125,6 +129,8 @@ func newScanCommand() *cobra.Command {
|
||||
"Project stream name (e.g. branch) to use in cloud")
|
||||
cmd.Flags().StringArrayVarP(&trustedRegistryUrls, "trusted-registry", "", []string{},
|
||||
"Trusted registry URLs to use for package manifest verification")
|
||||
cmd.Flags().BoolVarP(&scannerExperimental, "experimental", "", false,
|
||||
"Enable experimental features in scanner")
|
||||
|
||||
cmd.AddCommand(listParsersCommand())
|
||||
return cmd
|
||||
@ -302,6 +308,15 @@ func internalStartScan() error {
|
||||
reporters = append(reporters, rp)
|
||||
}
|
||||
|
||||
if !utils.IsEmptyString(graphReportDirectory) {
|
||||
rp, err := reporter.NewDotGraphReporter(graphReportDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reporters = append(reporters, rp)
|
||||
}
|
||||
|
||||
if !utils.IsEmptyString(csvReportPath) {
|
||||
rp, err := reporter.NewCsvReporter(reporter.CsvReportingConfig{
|
||||
Path: csvReportPath,
|
||||
@ -343,6 +358,7 @@ func internalStartScan() error {
|
||||
TransitiveDepth: transitiveDepth,
|
||||
ConcurrentAnalyzer: concurrency,
|
||||
ExcludePatterns: scanExclude,
|
||||
Experimental: scannerExperimental,
|
||||
}, readerList, enrichers, analyzers, reporters)
|
||||
|
||||
// Redirect log to files to create space for UI rendering
|
||||
|
||||
@ -13,3 +13,4 @@ bash $E2E_THIS_DIR/scenario-1-vet-scans-vet.sh
|
||||
bash $E2E_THIS_DIR/scenario-2-vet-scan-demo-client-java.sh
|
||||
bash $E2E_THIS_DIR/scenario-3-filter-fail-fast.sh
|
||||
bash $E2E_THIS_DIR/scenario-4-lfp-fail-fast.sh
|
||||
bash $E2E_THIS_DIR/scenario-5-gradle-depgraph-build.sh
|
||||
|
||||
@ -0,0 +1,147 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
antlr:antlr:2.7.7=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
ch.qos.logback:logback-classic:1.2.11=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
ch.qos.logback:logback-core:1.2.11=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-core:2.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-databind:2.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.module:jackson-module-parameter-names:2.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson:jackson-bom:2.13.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml:classmate:1.5.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.jayway.jsonpath:json-path:2.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.nimbusds:nimbus-jose-jwt:9.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.sun.activation:jakarta.activation:1.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
|
||||
com.sun.istack:istack-commons-runtime:3.0.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.thoughtworks.xstream:xstream:1.2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath
|
||||
com.zaxxer:HikariCP:4.0.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
commons-fileupload:commons-fileupload:1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.openfeign.form:feign-form-spring:3.8.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.openfeign.form:feign-form:3.8.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.openfeign:feign-core:11.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.openfeign:feign-slf4j:11.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-annotations:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-circuitbreaker:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-circularbuffer:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-consumer:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-core:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-framework-common:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-micrometer:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-ratelimiter:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-retry:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-spring-boot2:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-spring:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.resilience4j:resilience4j-timelimiter:1.7.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.vavr:vavr-match:0.10.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.vavr:vavr:0.10.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.activation:jakarta.activation-api:1.2.2=testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.annotation:jakarta.annotation-api:1.3.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.persistence:jakarta.persistence-api:2.2.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.transaction:jakarta.transaction-api:1.3.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.validation:jakarta.validation-api:2.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.xml.bind:jakarta.xml.bind-api:2.3.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.bytebuddy:byte-buddy-agent:1.12.17=testCompileClasspath,testRuntimeClasspath
|
||||
net.bytebuddy:byte-buddy:1.12.17=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.minidev:accessors-smart:2.4.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.minidev:json-smart:2.4.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.logging.log4j:log4j-api:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.logging.log4j:log4j-to-slf4j:2.17.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-core:9.0.65=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-el:9.0.65=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-websocket:9.0.65=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
|
||||
org.aspectj:aspectjweaver:1.9.7=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.assertj:assertj-core:3.22.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.atteo:evo-inflector:1.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.bouncycastle:bcpkix-jdk15on:1.69=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.bouncycastle:bcprov-jdk15on:1.69=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.bouncycastle:bcutil-jdk15on:1.69=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.glassfish.jaxb:jaxb-runtime:2.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.glassfish.jaxb:txw2:2.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.hibernate.common:hibernate-commons-annotations:5.1.2.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.hibernate.validator:hibernate-validator:6.2.5.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.hibernate:hibernate-core:5.6.11.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.jboss.logging:jboss-logging:3.4.3.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.jboss:jandex:2.4.2.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.jsoup:jsoup:1.15.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-api:5.8.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:5.8.2=testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-params:5.8.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter:5.8.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-commons:1.8.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-engine:1.8.2=testRuntimeClasspath
|
||||
org.junit:junit-bom:5.8.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.mockito:mockito-core:4.5.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.mockito:mockito-junit-jupiter:4.5.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.objenesis:objenesis:3.2=testRuntimeClasspath
|
||||
org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.ow2.asm:asm:9.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.projectlombok:lombok:1.18.24=annotationProcessor,compileClasspath
|
||||
org.skyscreamer:jsonassert:1.5.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:jul-to-slf4j:1.7.36=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-api:1.7.36=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-autoconfigure:2.7.4=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-configuration-processor:2.7.4=annotationProcessor,compileClasspath
|
||||
org.springframework.boot:spring-boot-devtools:2.7.4=developmentOnly,runtimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-aop:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-data-jpa:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-data-rest:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-jdbc:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-json:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-logging:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-oauth2-resource-server:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-security:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-test:2.7.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-validation:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-web:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter:2.7.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-test-autoconfigure:2.7.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-test:2.7.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot:2.7.4=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.cloud:spring-cloud-circuitbreaker-resilience4j:2.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.cloud:spring-cloud-commons:3.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.cloud:spring-cloud-context:3.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.cloud:spring-cloud-openfeign-core:3.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j:2.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.cloud:spring-cloud-starter-openfeign:3.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.cloud:spring-cloud-starter:3.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.data:spring-data-commons:2.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.data:spring-data-jpa:2.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.data:spring-data-rest-core:3.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.data:spring-data-rest-webmvc:3.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.hateoas:spring-hateoas:1.5.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.plugin:spring-plugin-core:2.0.0.RELEASE=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-config:5.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-core:5.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-crypto:5.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-core:5.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-jose:5.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-resource-server:5.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-rsa:1.0.11.RELEASE=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-test:5.7.3=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-web:5.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-aop:5.3.23=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-aspects:5.3.23=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-beans:5.3.23=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-context:5.3.23=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-core:5.3.23=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-expression:5.3.23=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-jcl:5.3.23=compileClasspath,developmentOnly,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-jdbc:5.3.23=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-orm:5.3.23=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-test:5.3.23=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-tx:5.3.23=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-web:5.3.23=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webmvc:5.3.23=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.xmlunit:xmlunit-core:2.9.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.yaml:snakeyaml:1.30=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
xpp3:xpp3_min:1.1.3.4.O=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
xstream:xstream:1.2.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
empty=testAnnotationProcessor
|
||||
17
test/scenarios/scenario-5-gradle-depgraph-build.sh
Normal file
17
test/scenarios/scenario-5-gradle-depgraph-build.sh
Normal file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
$E2E_VET_BINARY scan -s --no-banner \
|
||||
--lockfiles $E2E_FIXTURES/lockfiles/demo-client-java-gradle.lockfile \
|
||||
--lockfile-as gradle.lockfile \
|
||||
--report-graph /tmp/graph
|
||||
|
||||
graphFile=$(ls /tmp/graph/*demo-client-java-gradle*)
|
||||
|
||||
grep "org.springframework:spring-beans@5.3.23" $graphFile
|
||||
grep '"org.springframework.boot:spring-boot-starter-data-rest@2.7.4" -> "org.springframework.data:spring-data-rest-webmvc@3.7.3";' $graphFile
|
||||
|
||||
set +e
|
||||
grep '"org.springframework.boot:spring-boot-starter-json@2.7.4" -> "org.springframework:spring-beans@5.3.23";' $graphFile && exit 1
|
||||
exit 0
|
||||
Loading…
x
Reference in New Issue
Block a user