mirror of
https://github.com/safedep/vet.git
synced 2025-12-11 17:44:20 -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/jedib0t/go-pretty/v6 v6.4.9
|
||||||
github.com/kubescape/go-git-url v0.0.25
|
github.com/kubescape/go-git-url v0.0.25
|
||||||
github.com/package-url/packageurl-go v0.1.2
|
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/sirupsen/logrus v1.9.3
|
||||||
github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8
|
github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8
|
||||||
github.com/spdx/tools-golang v0.5.3
|
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/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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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-20240110142304-5970e3335464 h1:H0JZ9xOrJ9VTA4G4TFOAy6+P9hJ7QqAlIBSBdf5uU8c=
|
||||||
github.com/safedep/dry v0.0.0-20231024121814-ee8dd6ec7d93/go.mod h1:2XNr8V9meIULvK/pazBdD/KSqB5hsC6XTcnygwuQg4w=
|
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 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
|
||||||
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
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=
|
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() {
|
func loadExceptions() {
|
||||||
if globalExceptionsFile != "" {
|
if globalExceptionsFile == "" {
|
||||||
loader, err := exceptions.NewExceptionsFileLoader(globalExceptionsFile)
|
return
|
||||||
if err != nil {
|
}
|
||||||
logger.Fatalf("Exceptions loader: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = exceptions.Load(loader)
|
loader, err := exceptions.NewExceptionsFileLoader(globalExceptionsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("Exceptions loader: %v", err)
|
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"
|
jsonreportspec "github.com/safedep/vet/gen/jsonreport"
|
||||||
"github.com/safedep/vet/pkg/common/logger"
|
"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/models"
|
||||||
"github.com/safedep/vet/pkg/readers"
|
"github.com/safedep/vet/pkg/readers"
|
||||||
)
|
)
|
||||||
@ -23,6 +24,7 @@ type npmPackageLockPackage struct {
|
|||||||
Integrity string `json:"integrity"`
|
Integrity string `json:"integrity"`
|
||||||
Dev bool `json:"dev"`
|
Dev bool `json:"dev"`
|
||||||
Optional bool `json:"optional"`
|
Optional bool `json:"optional"`
|
||||||
|
Link bool `json:"link"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json
|
// https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json
|
||||||
@ -90,6 +92,12 @@ func (npm *npmLockfilePoisoningAnalyzer) Analyze(manifest *models.PackageManifes
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lockfilePackage.Link {
|
||||||
|
logger.Debugf("npmLockfilePoisoningAnalyzer: Skipping linked package [%s] for [%s]",
|
||||||
|
path, lockfilePackage.Resolved)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
packageName := npmNodeModulesPackagePathToName(path)
|
packageName := npmNodeModulesPackagePathToName(path)
|
||||||
if packageName == "" {
|
if packageName == "" {
|
||||||
logger.Warnf("npmLockfilePoisoningAnalyzer: Failed to extract package name from path %s", path)
|
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
|
// Extract the package name from the node_modules filesystem path
|
||||||
func npmNodeModulesPackagePathToName(path string) string {
|
func npmNodeModulesPackagePathToName(path string) string {
|
||||||
// Extract the package name from the node_modules filesystem path
|
return utils.NpmNodeModulesPackagePathToName(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 ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if URL follows the pkg name path convention as per NPM package registry
|
// 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) {
|
func TestNpmIsUrlFollowsPathConvention(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@ -52,6 +52,8 @@ func purlBuildLockfilePackageName(ecosystem lockfile.Ecosystem, group, name stri
|
|||||||
switch ecosystem {
|
switch ecosystem {
|
||||||
case lockfile.GoEcosystem, lockfile.NpmEcosystem:
|
case lockfile.GoEcosystem, lockfile.NpmEcosystem:
|
||||||
return fmt.Sprintf("%s/%s", group, name)
|
return fmt.Sprintf("%s/%s", group, name)
|
||||||
|
case lockfile.MavenEcosystem:
|
||||||
|
return fmt.Sprintf("%s:%s", group, name)
|
||||||
default:
|
default:
|
||||||
return name
|
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
|
// only for packages not in the exempted by exception rules
|
||||||
func AllowedPackages(manifest *models.PackageManifest,
|
func AllowedPackages(manifest *models.PackageManifest,
|
||||||
handler func(pkg *models.Package) error) error {
|
handler func(pkg *models.Package) error) error {
|
||||||
for _, pkg := range manifest.Packages {
|
packages := manifest.GetPackages()
|
||||||
|
for _, pkg := range packages {
|
||||||
res, err := Apply(pkg)
|
res, err := Apply(pkg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to evaluate exception for %s: %v",
|
logger.Errorf("Failed to evaluate exception for %s: %v",
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"cmp"
|
||||||
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
// We are using generics here to make the graph implementation
|
// We are using generics here to make the graph implementation
|
||||||
// not too coupled with our model types
|
// not too coupled with our model types
|
||||||
@ -13,6 +17,9 @@ type DependencyGraphNodeType interface {
|
|||||||
type DependencyGraphNode[T DependencyGraphNodeType] struct {
|
type DependencyGraphNode[T DependencyGraphNodeType] struct {
|
||||||
Data T `json:"data"`
|
Data T `json:"data"`
|
||||||
Children []T `json:"children"`
|
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
|
// Directed Acyclic Graph (DAG) representation of the package manifest
|
||||||
@ -21,6 +28,10 @@ type DependencyGraph[T DependencyGraphNodeType] struct {
|
|||||||
nodes map[string]*DependencyGraphNode[T]
|
nodes map[string]*DependencyGraphNode[T]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (node *DependencyGraphNode[T]) SetRoot(root bool) {
|
||||||
|
node.Root = root
|
||||||
|
}
|
||||||
|
|
||||||
func NewDependencyGraph[T DependencyGraphNodeType]() *DependencyGraph[T] {
|
func NewDependencyGraph[T DependencyGraphNodeType]() *DependencyGraph[T] {
|
||||||
return &DependencyGraph[T]{
|
return &DependencyGraph[T]{
|
||||||
present: false,
|
present: false,
|
||||||
@ -46,18 +57,32 @@ func (dg *DependencyGraph[T]) SetPresent(present bool) {
|
|||||||
dg.present = present
|
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
|
// AddDependency adds a dependency from one package to another
|
||||||
// Add an edge from [from] to [to]
|
// Add an edge from [from] to [to]
|
||||||
func (dg *DependencyGraph[T]) AddDependency(from, to T) {
|
func (dg *DependencyGraph[T]) AddDependency(from, to T) {
|
||||||
if _, ok := dg.nodes[from.Id()]; !ok {
|
fromNode := dg.findOrCreateNode(from)
|
||||||
dg.nodes[from.Id()] = &DependencyGraphNode[T]{Data: from, Children: []T{}}
|
toNode := dg.findOrCreateNode(to)
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := dg.nodes[to.Id()]; !ok {
|
fromNode.Children = append(fromNode.Children, toNode.Data)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDependencies returns the list of dependencies for the given package
|
// 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
|
// GetNodes returns the list of nodes in the graph
|
||||||
// This is useful when enumerating all packages
|
func (dg *DependencyGraph[T]) GetNodes() []*DependencyGraphNode[T] {
|
||||||
func (dg *DependencyGraph[T]) GetNodes() []T {
|
var nodes []*DependencyGraphNode[T]
|
||||||
var nodes []T
|
|
||||||
for _, node := range dg.nodes {
|
for _, node := range dg.nodes {
|
||||||
nodes = append(nodes, node.Data)
|
nodes = append(nodes, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alias for GetNodes
|
// GetPackages returns the list of packages in the graph
|
||||||
func (dg *DependencyGraph[T]) GetPackages() []T {
|
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
|
// 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
|
// is more relevant here because we want to update minimum number of root packages
|
||||||
func (dg *DependencyGraph[T]) PathToRoot(pkg T) []T {
|
func (dg *DependencyGraph[T]) PathToRoot(pkg T) []T {
|
||||||
var path []T
|
var path []T
|
||||||
for _, node := range dg.nodes {
|
|
||||||
if node.Data.Id() == pkg.Id() {
|
// If the package is not present in the graph, return an empty path
|
||||||
path = append(path, node.Data)
|
if node, ok := dg.nodes[pkg.Id()]; ok {
|
||||||
break
|
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 {
|
for len(path) > 0 {
|
||||||
node := path[len(path)-1]
|
node := path[len(path)-1]
|
||||||
dependents := dg.GetDependents(node)
|
dependents := dg.GetDependents(node)
|
||||||
@ -125,12 +161,46 @@ func (dg *DependencyGraph[T]) PathToRoot(pkg T) []T {
|
|||||||
break
|
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
|
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) {
|
func (dg *DependencyGraph[T]) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(&struct {
|
return json.Marshal(&struct {
|
||||||
Present bool `json:"present"`
|
Present bool `json:"present"`
|
||||||
|
|||||||
@ -63,10 +63,14 @@ func TestDependencyGraphGetNodes(t *testing.T) {
|
|||||||
dependencyGraphAddTestData(dg)
|
dependencyGraphAddTestData(dg)
|
||||||
|
|
||||||
nodes := dg.GetNodes()
|
nodes := dg.GetNodes()
|
||||||
assert.Contains(t, nodes, &dgTestNode{Name: "a"})
|
assert.Equal(t, 4, len(nodes))
|
||||||
assert.Contains(t, nodes, &dgTestNode{Name: "b"})
|
|
||||||
assert.Contains(t, nodes, &dgTestNode{Name: "c"})
|
nodeNames := []string{}
|
||||||
assert.Contains(t, nodes, &dgTestNode{Name: "d"})
|
for _, node := range nodes {
|
||||||
|
nodeNames = append(nodeNames, node.Data.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, []string{"a", "b", "c", "d"}, nodeNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDependencyGraphPathToRoot(t *testing.T) {
|
func TestDependencyGraphPathToRoot(t *testing.T) {
|
||||||
@ -90,9 +94,8 @@ func TestDependencyGraphMarshalJSON(t *testing.T) {
|
|||||||
dependencyGraphAddTestData(dg)
|
dependencyGraphAddTestData(dg)
|
||||||
dg.SetPresent(true)
|
dg.SetPresent(true)
|
||||||
|
|
||||||
json, err := json.Marshal(dg)
|
_, err := json.Marshal(dg)
|
||||||
assert.Nil(t, err)
|
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) {
|
func TestDependencyGraphUnmarshalJSON(t *testing.T) {
|
||||||
|
|||||||
@ -66,6 +66,7 @@ func (pm *PackageManifest) AddPackage(pkg *Package) {
|
|||||||
defer pm.m.Unlock()
|
defer pm.m.Unlock()
|
||||||
|
|
||||||
pm.Packages = append(pm.Packages, pkg)
|
pm.Packages = append(pm.Packages, pkg)
|
||||||
|
pm.DependencyGraph.AddNode(pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PackageManifest) GetPath() string {
|
func (pm *PackageManifest) GetPath() string {
|
||||||
@ -91,7 +92,7 @@ func (pm *PackageManifest) GetDisplayPath() string {
|
|||||||
// else fallsback to the [Packages] field
|
// else fallsback to the [Packages] field
|
||||||
func (pm *PackageManifest) GetPackages() []*Package {
|
func (pm *PackageManifest) GetPackages() []*Package {
|
||||||
if pm.DependencyGraph != nil && pm.DependencyGraph.Present() {
|
if pm.DependencyGraph != nil && pm.DependencyGraph.Present() {
|
||||||
return pm.DependencyGraph.GetNodes()
|
return pm.DependencyGraph.GetPackages()
|
||||||
}
|
}
|
||||||
|
|
||||||
return pm.Packages
|
return pm.Packages
|
||||||
@ -103,7 +104,7 @@ func (pm *PackageManifest) Id() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PackageManifest) GetPackagesCount() int {
|
func (pm *PackageManifest) GetPackagesCount() int {
|
||||||
return len(pm.Packages)
|
return len(pm.GetPackages())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PackageManifest) GetSpecEcosystem() modelspec.Ecosystem {
|
func (pm *PackageManifest) GetSpecEcosystem() modelspec.Ecosystem {
|
||||||
@ -185,12 +186,38 @@ func (p *Package) ShortName() string {
|
|||||||
strings.ToLower(p.Name), p.Version)
|
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{
|
return lockfile.PackageDetails{
|
||||||
Ecosystem: lockfile.Ecosystem(e),
|
Ecosystem: lockfile.Ecosystem(ecosystem),
|
||||||
CompareAs: lockfile.Ecosystem(e),
|
CompareAs: lockfile.Ecosystem(ecosystem),
|
||||||
Name: n,
|
Name: name,
|
||||||
Version: v,
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/google/osv-scanner/pkg/lockfile"
|
"github.com/google/osv-scanner/pkg/lockfile"
|
||||||
"github.com/safedep/vet/pkg/common/logger"
|
"github.com/safedep/vet/pkg/common/logger"
|
||||||
@ -39,6 +40,7 @@ var supportedEcosystems map[string]bool = map[string]bool{
|
|||||||
models.EcosystemSpdxSBOM: true,
|
models.EcosystemSpdxSBOM: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate these to graph parser
|
||||||
var customExperimentalParsers map[string]lockfile.PackageDetailsParser = map[string]lockfile.PackageDetailsParser{
|
var customExperimentalParsers map[string]lockfile.PackageDetailsParser = map[string]lockfile.PackageDetailsParser{
|
||||||
customParserTypePyWheel: parsePythonWheelDist,
|
customParserTypePyWheel: parsePythonWheelDist,
|
||||||
customParserCycloneDXSBOM: cdx.Parse,
|
customParserCycloneDXSBOM: cdx.Parse,
|
||||||
@ -49,17 +51,39 @@ var customExperimentalParsers map[string]lockfile.PackageDetailsParser = map[str
|
|||||||
type Parser interface {
|
type Parser interface {
|
||||||
Ecosystem() string
|
Ecosystem() string
|
||||||
Parse(lockfilePath string) (*models.PackageManifest, error)
|
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 {
|
type parserWrapper struct {
|
||||||
parser lockfile.PackageDetailsParser
|
graphParser dependencyGraphParser
|
||||||
parseAs string
|
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 {
|
func List(experimental bool) []string {
|
||||||
supportedParsers := make([]string, 0, 0)
|
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 {
|
for _, p := range parsers {
|
||||||
_, err := FindParser("", p)
|
_, err := FindParser("", p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -79,6 +103,18 @@ func List(experimental bool) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FindParser(lockfilePath, lockfileAs string) (Parser, error) {
|
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)
|
p, pa := lockfile.FindParser(lockfilePath, lockfileAs)
|
||||||
if p != nil {
|
if p != nil {
|
||||||
pw := &parserWrapper{parser: p, parseAs: pa}
|
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)
|
logger.Debugf("Trying to find parser in experimental parsers %s", lockfileAs)
|
||||||
if p, ok := customExperimentalParsers[lockfileAs]; ok {
|
if p, ok := customExperimentalParsers[lockfileAs]; ok {
|
||||||
pw := &parserWrapper{parser: p, parseAs: lockfileAs}
|
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)
|
logger.Debugf("No Parser found for the type %s", lockfileAs)
|
||||||
return nil, fmt.Errorf("no parser found with: %s for: %s", lockfileAs,
|
return nil, fmt.Errorf("no parser found with: %s for: %s", lockfileAs,
|
||||||
lockfilePath)
|
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 {
|
func (pw *parserWrapper) supported() bool {
|
||||||
return supportedEcosystems[pw.Ecosystem()]
|
return supportedEcosystems[pw.Ecosystem()]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pw *parserWrapper) Ecosystem() string {
|
func (pw *parserWrapper) Ecosystem() string {
|
||||||
logger.Debugf("Provided Lockfile Type %s", pw.parseAs)
|
|
||||||
switch pw.parseAs {
|
switch pw.parseAs {
|
||||||
case "Cargo.lock":
|
case "Cargo.lock":
|
||||||
return models.EcosystemCargo
|
return models.EcosystemCargo
|
||||||
@ -153,14 +203,21 @@ func (pw *parserWrapper) Ecosystem() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pw *parserWrapper) Parse(lockfilePath string) (*models.PackageManifest, error) {
|
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)
|
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)
|
packages, err := pw.parser(lockfilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pm, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pm := models.NewPackageManifest(lockfilePath, pw.Ecosystem())
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
pm.AddPackage(&models.Package{
|
pm.AddPackage(&models.Package{
|
||||||
PackageDetails: pkg,
|
PackageDetails: pkg,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -8,7 +9,7 @@ import (
|
|||||||
|
|
||||||
func TestListParser(t *testing.T) {
|
func TestListParser(t *testing.T) {
|
||||||
parsers := List(false)
|
parsers := List(false)
|
||||||
assert.Equal(t, 11, len(parsers))
|
assert.Equal(t, 12, len(parsers))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInvalidEcosystemMapping(t *testing.T) {
|
func TestInvalidEcosystemMapping(t *testing.T) {
|
||||||
@ -19,6 +20,9 @@ func TestInvalidEcosystemMapping(t *testing.T) {
|
|||||||
func TestEcosystemMapping(t *testing.T) {
|
func TestEcosystemMapping(t *testing.T) {
|
||||||
for _, lf := range List(false) {
|
for _, lf := range List(false) {
|
||||||
t.Run(lf, func(t *testing.T) {
|
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}
|
pw := &parserWrapper{parseAs: lf}
|
||||||
assert.NotEmpty(t, pw.Ecosystem())
|
assert.NotEmpty(t, pw.Ecosystem())
|
||||||
})
|
})
|
||||||
|
|||||||
@ -171,7 +171,7 @@ func (p *githubReader) processRemoteDependencyGraph(ctx context.Context, client
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(manifest.Packages) == 0 {
|
if manifest.GetPackagesCount() == 0 {
|
||||||
return errors.New("no packages identified from SBOM")
|
return errors.New("no packages identified from SBOM")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -77,7 +77,7 @@ func (p *jsonDumpReader) EnumManifests(handler func(*models.PackageManifest,
|
|||||||
manifest.Path = path
|
manifest.Path = path
|
||||||
|
|
||||||
// Fix manifest reference in each package
|
// Fix manifest reference in each package
|
||||||
for _, pkg := range manifest.Packages {
|
for _, pkg := range manifest.GetPackages() {
|
||||||
pkg.Manifest = &manifest
|
pkg.Manifest = &manifest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ func TestLockfileReaderEnumManifests(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"", // Auto detect from name
|
"", // Auto detect from name
|
||||||
nil,
|
nil,
|
||||||
errors.New("package-lock.json: invalid character"),
|
errors.New("invalid character"),
|
||||||
0,
|
0,
|
||||||
[]int{13},
|
[]int{13},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -25,10 +25,7 @@ func (p *purlReader) EnumManifests(handler func(*models.PackageManifest,
|
|||||||
}
|
}
|
||||||
|
|
||||||
pd := parsedPurl.GetPackageDetails()
|
pd := parsedPurl.GetPackageDetails()
|
||||||
pm := &models.PackageManifest{
|
pm := models.NewPackageManifest(p.purl, string(pd.Ecosystem))
|
||||||
Path: p.purl,
|
|
||||||
Ecosystem: string(pd.Ecosystem),
|
|
||||||
}
|
|
||||||
|
|
||||||
pm.AddPackage(&models.Package{
|
pm.AddPackage(&models.Package{
|
||||||
PackageDetails: pd,
|
PackageDetails: pd,
|
||||||
|
|||||||
@ -26,3 +26,47 @@ func TestPurlReader(t *testing.T) {
|
|||||||
|
|
||||||
assert.Nil(t, err)
|
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 (
|
import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/safedep/vet/pkg/analyzer"
|
"github.com/safedep/vet/pkg/analyzer"
|
||||||
"github.com/safedep/vet/pkg/common/logger"
|
"github.com/safedep/vet/pkg/common/logger"
|
||||||
@ -25,6 +26,8 @@ type csvRecord struct {
|
|||||||
manifestPath string
|
manifestPath string
|
||||||
packageName string
|
packageName string
|
||||||
packageVersion string
|
packageVersion string
|
||||||
|
introducedBy string
|
||||||
|
pathToRoot string
|
||||||
violationReason string
|
violationReason string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,12 +80,29 @@ func (r *csvReporter) Finish() error {
|
|||||||
continue
|
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{
|
records = append(records, csvRecord{
|
||||||
ecosystem: string(v.Package.Ecosystem),
|
ecosystem: string(v.Package.Ecosystem),
|
||||||
manifestPath: v.Manifest.GetDisplayPath(),
|
manifestPath: v.Manifest.GetDisplayPath(),
|
||||||
packageName: v.Package.GetName(),
|
packageName: v.Package.GetName(),
|
||||||
packageVersion: v.Package.GetVersion(),
|
packageVersion: v.Package.GetVersion(),
|
||||||
violationReason: msg,
|
violationReason: msg,
|
||||||
|
introducedBy: introducedBy,
|
||||||
|
pathToRoot: pathToRoot,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +129,9 @@ func (r *csvReporter) persistCsvRecords(records []csvRecord) error {
|
|||||||
"Manifest Path",
|
"Manifest Path",
|
||||||
"Package Name",
|
"Package Name",
|
||||||
"Package Version",
|
"Package Version",
|
||||||
"Filter Name"})
|
"Violation",
|
||||||
|
"Introduced By",
|
||||||
|
"Path To Root"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -119,6 +141,8 @@ func (r *csvReporter) persistCsvRecords(records []csvRecord) error {
|
|||||||
csvRecord.ecosystem, csvRecord.manifestPath,
|
csvRecord.ecosystem, csvRecord.manifestPath,
|
||||||
csvRecord.packageName, csvRecord.packageVersion,
|
csvRecord.packageName, csvRecord.packageVersion,
|
||||||
csvRecord.violationReason,
|
csvRecord.violationReason,
|
||||||
|
csvRecord.introducedBy,
|
||||||
|
csvRecord.pathToRoot,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
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
|
package reporter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jedib0t/go-pretty/v6/table"
|
"github.com/jedib0t/go-pretty/v6/table"
|
||||||
@ -296,18 +297,18 @@ func (r *summaryReporter) Finish() error {
|
|||||||
func (r *summaryReporter) sortedRemediations() []*summaryReporterRemediationData {
|
func (r *summaryReporter) sortedRemediations() []*summaryReporterRemediationData {
|
||||||
sortedPackages := []*summaryReporterRemediationData{}
|
sortedPackages := []*summaryReporterRemediationData{}
|
||||||
for _, value := range r.remediationScores {
|
for _, value := range r.remediationScores {
|
||||||
i := sort.Search(len(sortedPackages), func(i int) bool {
|
sortedPackages = append(sortedPackages, value)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return sortedPackages
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,6 +346,13 @@ func (r *summaryReporter) renderRemediationAdvice() {
|
|||||||
"", tagText, "", "",
|
"", tagText, "", "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
pathToRoot := text.Faint.Sprint(r.pathToPackageRoot(sp.pkg))
|
||||||
|
if pathToRoot != "" {
|
||||||
|
tbl.AppendRow(table.Row{
|
||||||
|
"", pathToRoot, "", "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
tbl.AppendSeparator()
|
tbl.AppendSeparator()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,3 +416,28 @@ func (r *summaryReporter) exceptionsCountStatement() string {
|
|||||||
return fmt.Sprintf("%d libraries are exempted from analysis through exception rules",
|
return fmt.Sprintf("%d libraries are exempted from analysis through exception rules",
|
||||||
exceptions.ActiveCount())
|
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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
dryutils "github.com/safedep/dry/utils"
|
||||||
"github.com/safedep/vet/pkg/analyzer"
|
"github.com/safedep/vet/pkg/analyzer"
|
||||||
"github.com/safedep/vet/pkg/common/logger"
|
"github.com/safedep/vet/pkg/common/logger"
|
||||||
"github.com/safedep/vet/pkg/common/utils"
|
"github.com/safedep/vet/pkg/common/utils"
|
||||||
@ -17,6 +18,7 @@ type Config struct {
|
|||||||
ConcurrentAnalyzer int
|
ConcurrentAnalyzer int
|
||||||
TransitiveAnalysis bool
|
TransitiveAnalysis bool
|
||||||
TransitiveDepth int
|
TransitiveDepth int
|
||||||
|
Experimental bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type packageManifestScanner struct {
|
type packageManifestScanner struct {
|
||||||
@ -213,6 +215,8 @@ func (s *packageManifestScanner) enrichManifest(manifest *models.PackageManifest
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer s.finaliseDependencyGraph(manifest)
|
||||||
|
|
||||||
// FIXME: Potential deadlock situation in case of channel buffer is full
|
// FIXME: Potential deadlock situation in case of channel buffer is full
|
||||||
// because the goroutines perform both read and write to channel. Write occurs
|
// because the goroutines perform both read and write to channel. Write occurs
|
||||||
// when goroutine invokes the work queue handler and the handler pushes back
|
// 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] {
|
func (s *packageManifestScanner) packageEnrichWorkQueueHandler(pm *models.PackageManifest) utils.WorkQueueFn[*models.Package] {
|
||||||
return func(q *utils.WorkQueue[*models.Package], item *models.Package) error {
|
return func(q *utils.WorkQueue[*models.Package], item *models.Package) error {
|
||||||
for _, enricher := range s.enrichers {
|
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 {
|
if err != nil {
|
||||||
logger.Errorf("Enricher %s failed with %v", enricher.Name(), err)
|
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,
|
func (s *packageManifestScanner) packageDependencyHandler(pm *models.PackageManifest,
|
||||||
|
currentPkg *models.Package,
|
||||||
q *utils.WorkQueue[*models.Package]) PackageDependencyCallbackFn {
|
q *utils.WorkQueue[*models.Package]) PackageDependencyCallbackFn {
|
||||||
return func(pkg *models.Package) error {
|
return func(pkg *models.Package) error {
|
||||||
|
// Check and queue for further analysis
|
||||||
if !s.config.TransitiveAnalysis {
|
if !s.config.TransitiveAnalysis {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -278,3 +284,61 @@ func (s *packageManifestScanner) packageDependencyHandler(pm *models.PackageMani
|
|||||||
return nil
|
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
|
querySummaryReportMaxAdvice int
|
||||||
queryMarkdownReportPath string
|
queryMarkdownReportPath string
|
||||||
queryJsonReportPath string
|
queryJsonReportPath string
|
||||||
|
queryGraphReportPath string
|
||||||
queryCsvReportPath string
|
queryCsvReportPath string
|
||||||
queryExceptionsFile string
|
queryExceptionsFile string
|
||||||
queryExceptionsTill string
|
queryExceptionsTill string
|
||||||
@ -64,6 +65,8 @@ func newQueryCommand() *cobra.Command {
|
|||||||
"Generate markdown report to file")
|
"Generate markdown report to file")
|
||||||
cmd.Flags().StringVarP(&queryJsonReportPath, "report-json", "", "",
|
cmd.Flags().StringVarP(&queryJsonReportPath, "report-json", "", "",
|
||||||
"Generate JSON report to file (EXPERIMENTAL)")
|
"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", "", "",
|
cmd.Flags().StringVarP(&queryCsvReportPath, "report-csv", "", "",
|
||||||
"Generate CSV report of filtered packages to file")
|
"Generate CSV report of filtered packages to file")
|
||||||
return cmd
|
return cmd
|
||||||
@ -176,6 +179,15 @@ func internalStartQuery() error {
|
|||||||
reporters = append(reporters, rp)
|
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{
|
pmScanner := scanner.NewPackageManifestScanner(scanner.Config{
|
||||||
TransitiveAnalysis: false,
|
TransitiveAnalysis: false,
|
||||||
}, readerList, enrichers, analyzers, reporters)
|
}, readerList, enrichers, analyzers, reporters)
|
||||||
|
|||||||
16
scan.go
16
scan.go
@ -46,10 +46,12 @@ var (
|
|||||||
disableAuthVerifyBeforeScan bool
|
disableAuthVerifyBeforeScan bool
|
||||||
syncReport bool
|
syncReport bool
|
||||||
syncReportProject string
|
syncReportProject string
|
||||||
|
graphReportDirectory string
|
||||||
syncReportStream string
|
syncReportStream string
|
||||||
listExperimentalParsers bool
|
listExperimentalParsers bool
|
||||||
failFast bool
|
failFast bool
|
||||||
trustedRegistryUrls []string
|
trustedRegistryUrls []string
|
||||||
|
scannerExperimental bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func newScanCommand() *cobra.Command {
|
func newScanCommand() *cobra.Command {
|
||||||
@ -117,6 +119,8 @@ func newScanCommand() *cobra.Command {
|
|||||||
"Generate CSV report of filtered packages")
|
"Generate CSV report of filtered packages")
|
||||||
cmd.Flags().StringVarP(&jsonReportPath, "report-json", "", "",
|
cmd.Flags().StringVarP(&jsonReportPath, "report-json", "", "",
|
||||||
"Generate consolidated JSON report to file (EXPERIMENTAL schema)")
|
"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,
|
cmd.Flags().BoolVarP(&syncReport, "report-sync", "", false,
|
||||||
"Enable syncing report data to cloud")
|
"Enable syncing report data to cloud")
|
||||||
cmd.Flags().StringVarP(&syncReportProject, "report-sync-project", "", "",
|
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")
|
"Project stream name (e.g. branch) to use in cloud")
|
||||||
cmd.Flags().StringArrayVarP(&trustedRegistryUrls, "trusted-registry", "", []string{},
|
cmd.Flags().StringArrayVarP(&trustedRegistryUrls, "trusted-registry", "", []string{},
|
||||||
"Trusted registry URLs to use for package manifest verification")
|
"Trusted registry URLs to use for package manifest verification")
|
||||||
|
cmd.Flags().BoolVarP(&scannerExperimental, "experimental", "", false,
|
||||||
|
"Enable experimental features in scanner")
|
||||||
|
|
||||||
cmd.AddCommand(listParsersCommand())
|
cmd.AddCommand(listParsersCommand())
|
||||||
return cmd
|
return cmd
|
||||||
@ -302,6 +308,15 @@ func internalStartScan() error {
|
|||||||
reporters = append(reporters, rp)
|
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) {
|
if !utils.IsEmptyString(csvReportPath) {
|
||||||
rp, err := reporter.NewCsvReporter(reporter.CsvReportingConfig{
|
rp, err := reporter.NewCsvReporter(reporter.CsvReportingConfig{
|
||||||
Path: csvReportPath,
|
Path: csvReportPath,
|
||||||
@ -343,6 +358,7 @@ func internalStartScan() error {
|
|||||||
TransitiveDepth: transitiveDepth,
|
TransitiveDepth: transitiveDepth,
|
||||||
ConcurrentAnalyzer: concurrency,
|
ConcurrentAnalyzer: concurrency,
|
||||||
ExcludePatterns: scanExclude,
|
ExcludePatterns: scanExclude,
|
||||||
|
Experimental: scannerExperimental,
|
||||||
}, readerList, enrichers, analyzers, reporters)
|
}, readerList, enrichers, analyzers, reporters)
|
||||||
|
|
||||||
// Redirect log to files to create space for UI rendering
|
// 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-2-vet-scan-demo-client-java.sh
|
||||||
bash $E2E_THIS_DIR/scenario-3-filter-fail-fast.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-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