Merge pull request #187 from safedep/feat/npm-graph-parser

feat: Add support for npm Dependency Graph
This commit is contained in:
Abhisek Datta 2024-01-22 10:31:19 +05:30 committed by GitHub
commit 774323c28f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 12201 additions and 128 deletions

2
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/jedib0t/go-pretty/v6 v6.4.9
github.com/kubescape/go-git-url v0.0.25
github.com/package-url/packageurl-go v0.1.2
github.com/safedep/dry v0.0.0-20231024121814-ee8dd6ec7d93
github.com/safedep/dry v0.0.0-20240110142304-5970e3335464
github.com/sirupsen/logrus v1.9.3
github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8
github.com/spdx/tools-golang v0.5.3

4
go.sum
View File

@ -222,8 +222,8 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safedep/dry v0.0.0-20231024121814-ee8dd6ec7d93 h1:SLJTcy8drjRSTg8T8yEI7RV0Fhz4U183s8VVB1fzlgk=
github.com/safedep/dry v0.0.0-20231024121814-ee8dd6ec7d93/go.mod h1:2XNr8V9meIULvK/pazBdD/KSqB5hsC6XTcnygwuQg4w=
github.com/safedep/dry v0.0.0-20240110142304-5970e3335464 h1:H0JZ9xOrJ9VTA4G4TFOAy6+P9hJ7QqAlIBSBdf5uU8c=
github.com/safedep/dry v0.0.0-20240110142304-5970e3335464/go.mod h1:2XNr8V9meIULvK/pazBdD/KSqB5hsC6XTcnygwuQg4w=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=

20
main.go
View File

@ -74,16 +74,18 @@ func main() {
}
func loadExceptions() {
if globalExceptionsFile != "" {
loader, err := exceptions.NewExceptionsFileLoader(globalExceptionsFile)
if err != nil {
logger.Fatalf("Exceptions loader: %v", err)
}
if globalExceptionsFile == "" {
return
}
err = exceptions.Load(loader)
if err != nil {
logger.Fatalf("Exceptions loader: %v", err)
}
loader, err := exceptions.NewExceptionsFileLoader(globalExceptionsFile)
if err != nil {
logger.Fatalf("Exceptions loader: %v", err)
}
err = exceptions.Load(loader)
if err != nil {
logger.Fatalf("Exceptions loader: %v", err)
}
}

View File

@ -10,6 +10,7 @@ import (
jsonreportspec "github.com/safedep/vet/gen/jsonreport"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/common/utils"
"github.com/safedep/vet/pkg/models"
"github.com/safedep/vet/pkg/readers"
)
@ -23,6 +24,7 @@ type npmPackageLockPackage struct {
Integrity string `json:"integrity"`
Dev bool `json:"dev"`
Optional bool `json:"optional"`
Link bool `json:"link"`
}
// https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json
@ -90,6 +92,12 @@ func (npm *npmLockfilePoisoningAnalyzer) Analyze(manifest *models.PackageManifes
continue
}
if lockfilePackage.Link {
logger.Debugf("npmLockfilePoisoningAnalyzer: Skipping linked package [%s] for [%s]",
path, lockfilePackage.Resolved)
continue
}
packageName := npmNodeModulesPackagePathToName(path)
if packageName == "" {
logger.Warnf("npmLockfilePoisoningAnalyzer: Failed to extract package name from path %s", path)
@ -229,19 +237,7 @@ func npmIsTrustedSource(sourceUrl string, trusteUrls []string) bool {
// Extract the package name from the node_modules filesystem path
func npmNodeModulesPackagePathToName(path string) string {
// Extract the package name from the node_modules filesystem path
// Example: node_modules/express -> express
// Example: node_modules/@angular/core -> @angular/core
// Example: node_modules/@angular/core/node_modules/express -> express
// Example: node_modules/@angular/core/node_modules/@angular/common -> @angular/common
for i := len(path) - 1; i >= 0; i-- {
if (len(path[i:]) > 13) && (path[i:i+13] == "node_modules/") {
return path[i+13:]
}
}
return ""
return utils.NpmNodeModulesPackagePathToName(path)
}
// Test if URL follows the pkg name path convention as per NPM package registry

View File

@ -83,47 +83,6 @@ func TestNpmIsTrustedSource(t *testing.T) {
}
}
func TestNpmNodeModulesPackagePathToName(t *testing.T) {
cases := []struct {
name string
path string
expected string
}{
{
"package name is extracted from path",
"/a/b/c/node_modules/package-name",
"package-name",
},
{
"node_modules relative",
"node_modules/express",
"express",
},
{
"node_modules relative scoped name",
"node_modules/@angular/core",
"@angular/core",
},
{
"nested node_modules relative",
"node_modules/@angular/core/node_modules/express",
"express",
},
{
"nested node_modules relative scoped name",
"node_modules/@angular/core/node_modules/@angular/common",
"@angular/common",
},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
actual := npmNodeModulesPackagePathToName(test.path)
assert.Equal(t, test.expected, actual)
})
}
}
func TestNpmIsUrlFollowsPathConvention(t *testing.T) {
cases := []struct {
name string

View File

@ -52,6 +52,8 @@ func purlBuildLockfilePackageName(ecosystem lockfile.Ecosystem, group, name stri
switch ecosystem {
case lockfile.GoEcosystem, lockfile.NpmEcosystem:
return fmt.Sprintf("%s/%s", group, name)
case lockfile.MavenEcosystem:
return fmt.Sprintf("%s:%s", group, name)
default:
return name
}

28
pkg/common/utils/graph.go Normal file
View 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
View 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
}

View 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)
})
}
}

View File

@ -9,7 +9,8 @@ import (
// only for packages not in the exempted by exception rules
func AllowedPackages(manifest *models.PackageManifest,
handler func(pkg *models.Package) error) error {
for _, pkg := range manifest.Packages {
packages := manifest.GetPackages()
for _, pkg := range packages {
res, err := Apply(pkg)
if err != nil {
logger.Errorf("Failed to evaluate exception for %s: %v",

View File

@ -1,6 +1,10 @@
package models
import "encoding/json"
import (
"cmp"
"encoding/json"
"slices"
)
// We are using generics here to make the graph implementation
// not too coupled with our model types
@ -13,6 +17,9 @@ type DependencyGraphNodeType interface {
type DependencyGraphNode[T DependencyGraphNodeType] struct {
Data T `json:"data"`
Children []T `json:"children"`
// While not relevant for a graph, this is required to identify root packages
Root bool `json:"root"`
}
// Directed Acyclic Graph (DAG) representation of the package manifest
@ -21,6 +28,10 @@ type DependencyGraph[T DependencyGraphNodeType] struct {
nodes map[string]*DependencyGraphNode[T]
}
func (node *DependencyGraphNode[T]) SetRoot(root bool) {
node.Root = root
}
func NewDependencyGraph[T DependencyGraphNodeType]() *DependencyGraph[T] {
return &DependencyGraph[T]{
present: false,
@ -46,18 +57,32 @@ func (dg *DependencyGraph[T]) SetPresent(present bool) {
dg.present = present
}
// Add a node to the graph
func (dg *DependencyGraph[T]) AddNode(node T) {
_ = dg.findOrCreateNode(node)
}
func (dg *DependencyGraph[T]) IsRoot(data T) bool {
if node, ok := dg.nodes[data.Id()]; ok {
return node.Root
}
return false
}
// Add a root node to the graph
func (dg *DependencyGraph[T]) AddRootNode(node T) {
dg.AddNode(node)
dg.nodes[node.Id()].Root = true
}
// AddDependency adds a dependency from one package to another
// Add an edge from [from] to [to]
func (dg *DependencyGraph[T]) AddDependency(from, to T) {
if _, ok := dg.nodes[from.Id()]; !ok {
dg.nodes[from.Id()] = &DependencyGraphNode[T]{Data: from, Children: []T{}}
}
fromNode := dg.findOrCreateNode(from)
toNode := dg.findOrCreateNode(to)
if _, ok := dg.nodes[to.Id()]; !ok {
dg.nodes[to.Id()] = &DependencyGraphNode[T]{Data: to, Children: []T{}}
}
dg.nodes[from.Id()].Children = append(dg.nodes[from.Id()].Children, dg.nodes[to.Id()].Data)
fromNode.Children = append(fromNode.Children, toNode.Data)
}
// GetDependencies returns the list of dependencies for the given package
@ -90,19 +115,23 @@ func (dg *DependencyGraph[T]) GetDependents(pkg T) []T {
}
// GetNodes returns the list of nodes in the graph
// This is useful when enumerating all packages
func (dg *DependencyGraph[T]) GetNodes() []T {
var nodes []T
func (dg *DependencyGraph[T]) GetNodes() []*DependencyGraphNode[T] {
var nodes []*DependencyGraphNode[T]
for _, node := range dg.nodes {
nodes = append(nodes, node.Data)
nodes = append(nodes, node)
}
return nodes
}
// Alias for GetNodes
// GetPackages returns the list of packages in the graph
func (dg *DependencyGraph[T]) GetPackages() []T {
return dg.GetNodes()
var packages []T
for _, node := range dg.nodes {
packages = append(packages, node.Data)
}
return packages
}
// PathToRoot returns the path from the given package to the root
@ -111,13 +140,20 @@ func (dg *DependencyGraph[T]) GetPackages() []T {
// is more relevant here because we want to update minimum number of root packages
func (dg *DependencyGraph[T]) PathToRoot(pkg T) []T {
var path []T
for _, node := range dg.nodes {
if node.Data.Id() == pkg.Id() {
path = append(path, node.Data)
break
}
// If the package is not present in the graph, return an empty path
if node, ok := dg.nodes[pkg.Id()]; ok {
path = append(path, node.Data)
} else {
return path
}
// Check if we are already at the root
if dg.nodes[pkg.Id()].Root {
return path
}
visited := make(map[string]bool)
for len(path) > 0 {
node := path[len(path)-1]
dependents := dg.GetDependents(node)
@ -125,12 +161,46 @@ func (dg *DependencyGraph[T]) PathToRoot(pkg T) []T {
break
}
path = append(path, dependents[0])
// Sort dependents by Id to ensure deterministic traversal
slices.SortFunc(dependents, func(a, b T) int {
return cmp.Compare(a.Id(), b.Id())
})
progress := false
for _, dependent := range dependents {
if _, ok := visited[dependent.Id()]; !ok {
path = append(path, dependent)
visited[dependent.Id()] = true
progress = true
break
}
}
if !progress {
break
}
if n, ok := dg.nodes[path[len(path)-1].Id()]; ok && n.Root {
break
}
}
return path
}
func (dg *DependencyGraph[T]) findOrCreateNode(data T) *DependencyGraphNode[T] {
id := data.Id()
if _, ok := dg.nodes[id]; !ok {
dg.nodes[id] = &DependencyGraphNode[T]{
Root: false,
Data: data,
Children: []T{},
}
}
return dg.nodes[id]
}
func (dg *DependencyGraph[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Present bool `json:"present"`

View File

@ -63,10 +63,14 @@ func TestDependencyGraphGetNodes(t *testing.T) {
dependencyGraphAddTestData(dg)
nodes := dg.GetNodes()
assert.Contains(t, nodes, &dgTestNode{Name: "a"})
assert.Contains(t, nodes, &dgTestNode{Name: "b"})
assert.Contains(t, nodes, &dgTestNode{Name: "c"})
assert.Contains(t, nodes, &dgTestNode{Name: "d"})
assert.Equal(t, 4, len(nodes))
nodeNames := []string{}
for _, node := range nodes {
nodeNames = append(nodeNames, node.Data.Name)
}
assert.ElementsMatch(t, []string{"a", "b", "c", "d"}, nodeNames)
}
func TestDependencyGraphPathToRoot(t *testing.T) {
@ -90,9 +94,8 @@ func TestDependencyGraphMarshalJSON(t *testing.T) {
dependencyGraphAddTestData(dg)
dg.SetPresent(true)
json, err := json.Marshal(dg)
_, err := json.Marshal(dg)
assert.Nil(t, err)
assert.Equal(t, "{\"present\":true,\"nodes\":{\"a\":{\"data\":{\"Name\":\"a\"},\"children\":[{\"Name\":\"b\"},{\"Name\":\"c\"}]},\"b\":{\"data\":{\"Name\":\"b\"},\"children\":[{\"Name\":\"c\"}]},\"c\":{\"data\":{\"Name\":\"c\"},\"children\":[{\"Name\":\"d\"}]},\"d\":{\"data\":{\"Name\":\"d\"},\"children\":[]}}}", string(json))
}
func TestDependencyGraphUnmarshalJSON(t *testing.T) {

View File

@ -66,6 +66,7 @@ func (pm *PackageManifest) AddPackage(pkg *Package) {
defer pm.m.Unlock()
pm.Packages = append(pm.Packages, pkg)
pm.DependencyGraph.AddNode(pkg)
}
func (pm *PackageManifest) GetPath() string {
@ -91,7 +92,7 @@ func (pm *PackageManifest) GetDisplayPath() string {
// else fallsback to the [Packages] field
func (pm *PackageManifest) GetPackages() []*Package {
if pm.DependencyGraph != nil && pm.DependencyGraph.Present() {
return pm.DependencyGraph.GetNodes()
return pm.DependencyGraph.GetPackages()
}
return pm.Packages
@ -103,7 +104,7 @@ func (pm *PackageManifest) Id() string {
}
func (pm *PackageManifest) GetPackagesCount() int {
return len(pm.Packages)
return len(pm.GetPackages())
}
func (pm *PackageManifest) GetSpecEcosystem() modelspec.Ecosystem {
@ -185,12 +186,38 @@ func (p *Package) ShortName() string {
strings.ToLower(p.Name), p.Version)
}
func NewPackageDetail(e, n, v string) lockfile.PackageDetails {
func (p *Package) GetDependencyGraph() *DependencyGraph[*Package] {
if p.Manifest == nil {
return nil
}
if p.Manifest.DependencyGraph == nil {
return nil
}
if !p.Manifest.DependencyGraph.Present() {
return nil
}
return p.Manifest.DependencyGraph
}
// DependencyPath returns the path from a root package to this package
func (p *Package) DependencyPath() []*Package {
dg := p.GetDependencyGraph()
if dg == nil {
return []*Package{}
}
return dg.PathToRoot(p)
}
func NewPackageDetail(ecosystem, name, version string) lockfile.PackageDetails {
return lockfile.PackageDetails{
Ecosystem: lockfile.Ecosystem(e),
CompareAs: lockfile.Ecosystem(e),
Name: n,
Version: v,
Ecosystem: lockfile.Ecosystem(ecosystem),
CompareAs: lockfile.Ecosystem(ecosystem),
Name: name,
Version: version,
}
}

File diff suppressed because it is too large Load Diff

135
pkg/parser/npm_graph.go Normal file
View 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)
}

View 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())
}

View File

@ -2,6 +2,7 @@ package parser
import (
"fmt"
"path/filepath"
"github.com/google/osv-scanner/pkg/lockfile"
"github.com/safedep/vet/pkg/common/logger"
@ -39,6 +40,7 @@ var supportedEcosystems map[string]bool = map[string]bool{
models.EcosystemSpdxSBOM: true,
}
// TODO: Migrate these to graph parser
var customExperimentalParsers map[string]lockfile.PackageDetailsParser = map[string]lockfile.PackageDetailsParser{
customParserTypePyWheel: parsePythonWheelDist,
customParserCycloneDXSBOM: cdx.Parse,
@ -49,17 +51,39 @@ var customExperimentalParsers map[string]lockfile.PackageDetailsParser = map[str
type Parser interface {
Ecosystem() string
Parse(lockfilePath string) (*models.PackageManifest, error)
ParseWithConfig(lockfilePath string, config *ParserConfig) (*models.PackageManifest, error)
}
type ParserConfig struct {
// A generic config flag (not specific to npm even though the name sounds like that) to indicate
// if the parser should include non-production dependencies as well. But this will work
// only for supported parsers such as npm graph parser
IncludeDevDependencies bool
}
// Graph parser always takes precedence over lockfile parser
type parserWrapper struct {
parser lockfile.PackageDetailsParser
parseAs string
graphParser dependencyGraphParser
parser lockfile.PackageDetailsParser
parseAs string
}
// This is how a graph parser should be implemented
type dependencyGraphParser func(lockfilePath string, config *ParserConfig) (*models.PackageManifest, error)
// Maintain a map of lockfileAs to dependencyGraphParser
var dependencyGraphParsers map[string]dependencyGraphParser = map[string]dependencyGraphParser{
"package-lock.json": parseNpmPackageLockAsGraph,
}
func List(experimental bool) []string {
supportedParsers := make([]string, 0, 0)
parsers := lockfile.ListParsers()
for pa := range dependencyGraphParsers {
supportedParsers = append(supportedParsers, fmt.Sprintf("%s (graph)", pa))
}
parsers := lockfile.ListParsers()
for _, p := range parsers {
_, err := FindParser("", p)
if err != nil {
@ -79,6 +103,18 @@ func List(experimental bool) []string {
}
func FindParser(lockfilePath, lockfileAs string) (Parser, error) {
// Find a graph parser for the lockfile
logger.Debugf("Trying to find graph parser for %s", lockfilePath)
gp, gpa := findGraphParser(lockfilePath, lockfileAs)
if gp != nil {
pw := &parserWrapper{graphParser: gp, parseAs: gpa}
if pw.supported() {
return pw, nil
}
}
// Try to find a parser for the lockfile
logger.Debugf("Trying to find lockfile parser for %s", lockfilePath)
p, pa := lockfile.FindParser(lockfilePath, lockfileAs)
if p != nil {
pw := &parserWrapper{parser: p, parseAs: pa}
@ -87,6 +123,7 @@ func FindParser(lockfilePath, lockfileAs string) (Parser, error) {
}
}
// Use experimental parser for explicitly provided lockfile type
logger.Debugf("Trying to find parser in experimental parsers %s", lockfileAs)
if p, ok := customExperimentalParsers[lockfileAs]; ok {
pw := &parserWrapper{parser: p, parseAs: lockfileAs}
@ -96,17 +133,30 @@ func FindParser(lockfilePath, lockfileAs string) (Parser, error) {
}
}
// We failed!
logger.Debugf("No Parser found for the type %s", lockfileAs)
return nil, fmt.Errorf("no parser found with: %s for: %s", lockfileAs,
lockfilePath)
}
func findGraphParser(lockfilePath, lockfileAs string) (dependencyGraphParser, string) {
parseAs := lockfileAs
if lockfileAs == "" {
parseAs = filepath.Base(lockfilePath)
}
if _, ok := dependencyGraphParsers[parseAs]; ok {
return dependencyGraphParsers[parseAs], parseAs
}
return nil, ""
}
func (pw *parserWrapper) supported() bool {
return supportedEcosystems[pw.Ecosystem()]
}
func (pw *parserWrapper) Ecosystem() string {
logger.Debugf("Provided Lockfile Type %s", pw.parseAs)
switch pw.parseAs {
case "Cargo.lock":
return models.EcosystemCargo
@ -153,14 +203,21 @@ func (pw *parserWrapper) Ecosystem() string {
}
func (pw *parserWrapper) Parse(lockfilePath string) (*models.PackageManifest, error) {
return pw.ParseWithConfig(lockfilePath, &ParserConfig{})
}
func (pw *parserWrapper) ParseWithConfig(lockfilePath string, config *ParserConfig) (*models.PackageManifest, error) {
logger.Infof("[%s] Parsing %s", pw.parseAs, lockfilePath)
pm := models.NewPackageManifest(lockfilePath, pw.Ecosystem())
if pw.graphParser != nil {
return pw.graphParser(lockfilePath, config)
}
packages, err := pw.parser(lockfilePath)
if err != nil {
return pm, err
return nil, err
}
pm := models.NewPackageManifest(lockfilePath, pw.Ecosystem())
for _, pkg := range packages {
pm.AddPackage(&models.Package{
PackageDetails: pkg,

View File

@ -1,6 +1,7 @@
package parser
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -8,7 +9,7 @@ import (
func TestListParser(t *testing.T) {
parsers := List(false)
assert.Equal(t, 11, len(parsers))
assert.Equal(t, 12, len(parsers))
}
func TestInvalidEcosystemMapping(t *testing.T) {
@ -19,6 +20,9 @@ func TestInvalidEcosystemMapping(t *testing.T) {
func TestEcosystemMapping(t *testing.T) {
for _, lf := range List(false) {
t.Run(lf, func(t *testing.T) {
// For graph parsers, we add a tag to the end of the name
lf = strings.Split(lf, " ")[0]
pw := &parserWrapper{parseAs: lf}
assert.NotEmpty(t, pw.Ecosystem())
})

View File

@ -171,7 +171,7 @@ func (p *githubReader) processRemoteDependencyGraph(ctx context.Context, client
return err
}
if len(manifest.Packages) == 0 {
if manifest.GetPackagesCount() == 0 {
return errors.New("no packages identified from SBOM")
}

View File

@ -77,7 +77,7 @@ func (p *jsonDumpReader) EnumManifests(handler func(*models.PackageManifest,
manifest.Path = path
// Fix manifest reference in each package
for _, pkg := range manifest.Packages {
for _, pkg := range manifest.GetPackages() {
pkg.Manifest = &manifest
}

View File

@ -63,7 +63,7 @@ func TestLockfileReaderEnumManifests(t *testing.T) {
},
"", // Auto detect from name
nil,
errors.New("package-lock.json: invalid character"),
errors.New("invalid character"),
0,
[]int{13},
},

View File

@ -25,10 +25,7 @@ func (p *purlReader) EnumManifests(handler func(*models.PackageManifest,
}
pd := parsedPurl.GetPackageDetails()
pm := &models.PackageManifest{
Path: p.purl,
Ecosystem: string(pd.Ecosystem),
}
pm := models.NewPackageManifest(p.purl, string(pd.Ecosystem))
pm.AddPackage(&models.Package{
PackageDetails: pd,

View File

@ -26,3 +26,47 @@ func TestPurlReader(t *testing.T) {
assert.Nil(t, err)
}
func TestPurlReaderWithMultiplePURLS(t *testing.T) {
cases := []struct {
name string
purl string
ecosystem string
pkgName string
version string
}{
{
"Maven PURL",
"pkg:maven/org.apache.commons/commons-lang3@3.8.1",
"Maven",
"org.apache.commons:commons-lang3",
"3.8.1",
},
{
"Maven PURL log4j",
"pkg:maven/log4j/log4j@1.2.17",
"Maven",
"log4j:log4j",
"1.2.17",
},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
reader, err := NewPurlReader(test.purl)
assert.Nil(t, err)
err = reader.EnumManifests(func(pm *models.PackageManifest, pr PackageReader) error {
assert.Equal(t, 1, len(pm.Packages))
assert.NotNil(t, pm.Packages[0])
assert.Equal(t, test.pkgName, pm.Packages[0].Name)
assert.Equal(t, test.version, pm.Packages[0].Version)
assert.Equal(t, test.ecosystem, string(pm.Packages[0].Ecosystem))
return nil
})
assert.Nil(t, err)
})
}
}

View File

@ -3,6 +3,7 @@ package reporter
import (
"encoding/csv"
"os"
"strings"
"github.com/safedep/vet/pkg/analyzer"
"github.com/safedep/vet/pkg/common/logger"
@ -25,6 +26,8 @@ type csvRecord struct {
manifestPath string
packageName string
packageVersion string
introducedBy string
pathToRoot string
violationReason string
}
@ -77,12 +80,29 @@ func (r *csvReporter) Finish() error {
continue
}
introducedBy := ""
pathToRoot := ""
paths := v.Package.DependencyPath()
pathPackages := []string{}
for _, path := range paths {
pathPackages = append(pathPackages, path.GetName())
}
if len(paths) > 0 {
introducedBy = pathPackages[len(paths)-1]
pathToRoot = strings.Join(pathPackages, " -> ")
}
records = append(records, csvRecord{
ecosystem: string(v.Package.Ecosystem),
manifestPath: v.Manifest.GetDisplayPath(),
packageName: v.Package.GetName(),
packageVersion: v.Package.GetVersion(),
violationReason: msg,
introducedBy: introducedBy,
pathToRoot: pathToRoot,
})
}
@ -109,7 +129,9 @@ func (r *csvReporter) persistCsvRecords(records []csvRecord) error {
"Manifest Path",
"Package Name",
"Package Version",
"Filter Name"})
"Violation",
"Introduced By",
"Path To Root"})
if err != nil {
return err
}
@ -119,6 +141,8 @@ func (r *csvReporter) persistCsvRecords(records []csvRecord) error {
csvRecord.ecosystem, csvRecord.manifestPath,
csvRecord.packageName, csvRecord.packageVersion,
csvRecord.violationReason,
csvRecord.introducedBy,
csvRecord.pathToRoot,
}); err != nil {
return err
}

107
pkg/reporter/dot_graph.go Normal file
View 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())
}

View File

@ -1,9 +1,10 @@
package reporter
import (
"cmp"
"fmt"
"os"
"sort"
"slices"
"strings"
"github.com/jedib0t/go-pretty/v6/table"
@ -296,18 +297,18 @@ func (r *summaryReporter) Finish() error {
func (r *summaryReporter) sortedRemediations() []*summaryReporterRemediationData {
sortedPackages := []*summaryReporterRemediationData{}
for _, value := range r.remediationScores {
i := sort.Search(len(sortedPackages), func(i int) bool {
return value.score >= sortedPackages[i].score
})
if i == len(sortedPackages) {
sortedPackages = append(sortedPackages, value)
} else {
sortedPackages = append(sortedPackages[:i+1], sortedPackages[i:]...)
sortedPackages[i] = value
}
sortedPackages = append(sortedPackages, value)
}
slices.SortFunc(sortedPackages, func(a, b *summaryReporterRemediationData) int {
if a.score == b.score {
return cmp.Compare(a.pkg.GetName(), b.pkg.GetName())
}
// We want to sort by descending order
return cmp.Compare(b.score, a.score)
})
return sortedPackages
}
@ -345,6 +346,13 @@ func (r *summaryReporter) renderRemediationAdvice() {
"", tagText, "", "",
})
pathToRoot := text.Faint.Sprint(r.pathToPackageRoot(sp.pkg))
if pathToRoot != "" {
tbl.AppendRow(table.Row{
"", pathToRoot, "", "",
})
}
tbl.AppendSeparator()
}
@ -408,3 +416,28 @@ func (r *summaryReporter) exceptionsCountStatement() string {
return fmt.Sprintf("%d libraries are exempted from analysis through exception rules",
exceptions.ActiveCount())
}
func (r *summaryReporter) pathToPackageRoot(pkg *models.Package) string {
path := strings.Builder{}
dg := pkg.GetDependencyGraph()
if dg == nil {
return path.String()
}
if dg.IsRoot(pkg) {
return path.String()
}
pathToRoot := pkg.Manifest.DependencyGraph.PathToRoot(pkg)
if len(pathToRoot) == 0 {
return path.String()
}
path.WriteString(fmt.Sprintf(" ... [%d] > ", len(pathToRoot)-1))
rootPkg := pathToRoot[len(pathToRoot)-1]
path.WriteString(rootPkg.GetName() + "@" + rootPkg.GetVersion())
return path.String()
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
dryutils "github.com/safedep/dry/utils"
"github.com/safedep/vet/pkg/analyzer"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/common/utils"
@ -17,6 +18,7 @@ type Config struct {
ConcurrentAnalyzer int
TransitiveAnalysis bool
TransitiveDepth int
Experimental bool
}
type packageManifestScanner struct {
@ -213,6 +215,8 @@ func (s *packageManifestScanner) enrichManifest(manifest *models.PackageManifest
return nil
}
defer s.finaliseDependencyGraph(manifest)
// FIXME: Potential deadlock situation in case of channel buffer is full
// because the goroutines perform both read and write to channel. Write occurs
// when goroutine invokes the work queue handler and the handler pushes back
@ -246,7 +250,7 @@ func (s *packageManifestScanner) enrichManifest(manifest *models.PackageManifest
func (s *packageManifestScanner) packageEnrichWorkQueueHandler(pm *models.PackageManifest) utils.WorkQueueFn[*models.Package] {
return func(q *utils.WorkQueue[*models.Package], item *models.Package) error {
for _, enricher := range s.enrichers {
err := enricher.Enrich(item, s.packageDependencyHandler(pm, q))
err := enricher.Enrich(item, s.packageDependencyHandler(pm, item, q))
if err != nil {
logger.Errorf("Enricher %s failed with %v", enricher.Name(), err)
}
@ -257,8 +261,10 @@ func (s *packageManifestScanner) packageEnrichWorkQueueHandler(pm *models.Packag
}
func (s *packageManifestScanner) packageDependencyHandler(pm *models.PackageManifest,
currentPkg *models.Package,
q *utils.WorkQueue[*models.Package]) PackageDependencyCallbackFn {
return func(pkg *models.Package) error {
// Check and queue for further analysis
if !s.config.TransitiveAnalysis {
return nil
}
@ -278,3 +284,61 @@ func (s *packageManifestScanner) packageDependencyHandler(pm *models.PackageMani
return nil
}
}
// finaliseDependencyGraph attempts to mark some nodes as root node if they do not have a dependent
// this is just a heuristic to mark some nodes as root node. This may not be accurate in all cases
func (s *packageManifestScanner) finaliseDependencyGraph(manifest *models.PackageManifest) {
if manifest.DependencyGraph == nil {
return
}
if manifest.DependencyGraph.Present() {
return
}
// Building dependency graph using package insights
packages := manifest.GetPackages()
for _, pkg := range packages {
insights := dryutils.SafelyGetValue(pkg.Insights)
dependencies := dryutils.SafelyGetValue(insights.Dependencies)
for _, dep := range dependencies {
distance := dryutils.SafelyGetValue(dep.Distance)
// Distance = 0 is the package itself
if distance == 0 {
continue
}
// Distance > 1 means the package is not a direct dependency
if distance > 1 {
continue
}
logger.Debugf("Adding dependency %s/%s to dependency graph",
dep.PackageVersion.Name, dep.PackageVersion.Version)
targetPkg := utils.FindDependencyGraphNodeBySemverRange(manifest.DependencyGraph,
dep.PackageVersion.Name, dep.PackageVersion.Version)
if targetPkg == nil {
logger.Debugf("Dependency %s/%s not found in dependency graph",
dep.PackageVersion.Name, dep.PackageVersion.Version)
continue
}
manifest.DependencyGraph.AddDependency(pkg, targetPkg.Data)
}
}
nodes := manifest.DependencyGraph.GetNodes()
for _, node := range nodes {
pkg := node.Data
dependents := manifest.DependencyGraph.GetDependents(pkg)
if len(dependents) == 0 {
node.SetRoot(true)
}
}
manifest.DependencyGraph.SetPresent(true)
}

View File

@ -21,6 +21,7 @@ var (
querySummaryReportMaxAdvice int
queryMarkdownReportPath string
queryJsonReportPath string
queryGraphReportPath string
queryCsvReportPath string
queryExceptionsFile string
queryExceptionsTill string
@ -64,6 +65,8 @@ func newQueryCommand() *cobra.Command {
"Generate markdown report to file")
cmd.Flags().StringVarP(&queryJsonReportPath, "report-json", "", "",
"Generate JSON report to file (EXPERIMENTAL)")
cmd.Flags().StringVarP(&queryGraphReportPath, "report-graph", "", "",
"Generate dependency graph as graphviz dot files to directory")
cmd.Flags().StringVarP(&queryCsvReportPath, "report-csv", "", "",
"Generate CSV report of filtered packages to file")
return cmd
@ -176,6 +179,15 @@ func internalStartQuery() error {
reporters = append(reporters, rp)
}
if !utils.IsEmptyString(queryGraphReportPath) {
rp, err := reporter.NewDotGraphReporter(queryGraphReportPath)
if err != nil {
return err
}
reporters = append(reporters, rp)
}
pmScanner := scanner.NewPackageManifestScanner(scanner.Config{
TransitiveAnalysis: false,
}, readerList, enrichers, analyzers, reporters)

16
scan.go
View File

@ -46,10 +46,12 @@ var (
disableAuthVerifyBeforeScan bool
syncReport bool
syncReportProject string
graphReportDirectory string
syncReportStream string
listExperimentalParsers bool
failFast bool
trustedRegistryUrls []string
scannerExperimental bool
)
func newScanCommand() *cobra.Command {
@ -117,6 +119,8 @@ func newScanCommand() *cobra.Command {
"Generate CSV report of filtered packages")
cmd.Flags().StringVarP(&jsonReportPath, "report-json", "", "",
"Generate consolidated JSON report to file (EXPERIMENTAL schema)")
cmd.Flags().StringVarP(&graphReportDirectory, "report-graph", "", "",
"Generate dependency graph (if available) as dot files to directory")
cmd.Flags().BoolVarP(&syncReport, "report-sync", "", false,
"Enable syncing report data to cloud")
cmd.Flags().StringVarP(&syncReportProject, "report-sync-project", "", "",
@ -125,6 +129,8 @@ func newScanCommand() *cobra.Command {
"Project stream name (e.g. branch) to use in cloud")
cmd.Flags().StringArrayVarP(&trustedRegistryUrls, "trusted-registry", "", []string{},
"Trusted registry URLs to use for package manifest verification")
cmd.Flags().BoolVarP(&scannerExperimental, "experimental", "", false,
"Enable experimental features in scanner")
cmd.AddCommand(listParsersCommand())
return cmd
@ -302,6 +308,15 @@ func internalStartScan() error {
reporters = append(reporters, rp)
}
if !utils.IsEmptyString(graphReportDirectory) {
rp, err := reporter.NewDotGraphReporter(graphReportDirectory)
if err != nil {
return err
}
reporters = append(reporters, rp)
}
if !utils.IsEmptyString(csvReportPath) {
rp, err := reporter.NewCsvReporter(reporter.CsvReportingConfig{
Path: csvReportPath,
@ -343,6 +358,7 @@ func internalStartScan() error {
TransitiveDepth: transitiveDepth,
ConcurrentAnalyzer: concurrency,
ExcludePatterns: scanExclude,
Experimental: scannerExperimental,
}, readerList, enrichers, analyzers, reporters)
// Redirect log to files to create space for UI rendering

View File

@ -13,3 +13,4 @@ bash $E2E_THIS_DIR/scenario-1-vet-scans-vet.sh
bash $E2E_THIS_DIR/scenario-2-vet-scan-demo-client-java.sh
bash $E2E_THIS_DIR/scenario-3-filter-fail-fast.sh
bash $E2E_THIS_DIR/scenario-4-lfp-fail-fast.sh
bash $E2E_THIS_DIR/scenario-5-gradle-depgraph-build.sh

View File

@ -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

View 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